@mic-rexjs/usecases-react
Version:
React-based solution for use usecases of Clean Architecture
1,627 lines • 69.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { objectUseCase, entityUseCase, } from '@mic-rexjs/usecases';
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
import { renderHook, act, render, fireEvent, screen } from '@testing-library/react';
import { useUseCase } from '.';
import { useDeepCompareEffect, useMemoizedFn, useMount, useUpdate, useUpdateEffect } from 'ahooks';
import { Fragment, useEffect, useRef, useState } from 'react';
const EXT_1 = '.png';
const EXT_2 = '.jpg';
const PATH_1 = `hello${EXT_1}`;
const PATH_2 = `hello${EXT_2}`;
const PARENT_BUTTON_TEXT = 'parent button';
const CHILD_BUTTON_TEXT = 'child button';
const defaultFile = {
path: '',
ext: '',
size: 0,
};
const fileUseCase = (options = {}) => {
const entityReducers = objectUseCase();
const { pathPrefix = '', onPathChange } = options;
const init = function* (entity, newEntity = defaultFile) {
yield () => {
return newEntity;
};
};
const setPath = function* (entity, path) {
const { path: oldPath } = entity;
const newPath = pathPrefix + path;
yield {
...entity,
path: newPath,
ext: path.match(/\.[^.]+$/)?.[0] || '',
};
onPathChange?.(newPath, oldPath);
};
const readFile = async function* (entity, path) {
const size = 2000;
yield* setPath(entity, path);
await Promise.resolve(null);
yield (oldEntity) => {
return {
...oldEntity,
size,
};
};
return size;
};
const isImage = (entity) => {
const { ext } = entity;
return [EXT_1, EXT_2].includes(ext);
};
return { ...entityReducers, init, setPath, readFile, isImage };
};
const subtractionReducer = jest.fn((value1, value2) => {
return value1 - value2;
});
const mathUseCase = (options = {}) => {
const { extraValue = 0 } = options;
const add = (value1, value2) => {
return value1 + value2 + extraValue;
};
return { add, subtraction: subtractionReducer };
};
const fieldPathUseCase = () => {
return objectUseCase();
};
const Child = ({ textPrefix = '', onUpdate, onPathChange, onUndefinedEntity }) => {
const [file, { setPath }] = useUseCase(fileUseCase);
const { path, ext } = file || {};
const onClick = () => {
setPath(PATH_2);
};
const undefinedEntity = useMemoizedFn(() => {
onUndefinedEntity?.();
});
useUpdateEffect(() => {
onUpdate?.();
});
useEffect(() => {
if (file === void 0) {
undefinedEntity();
}
}, [file, undefinedEntity]);
const pathChange = useMemoizedFn(() => {
onPathChange?.(path);
});
useUpdateEffect(pathChange, [path, pathChange]);
return (_jsxs("div", { children: [_jsxs("main", { children: ["child path: ", path, _jsx("br", {}), "child ext: ", ext] }), _jsx("footer", { children: _jsx("button", { onClick: onClick, children: textPrefix + CHILD_BUTTON_TEXT }) })] }));
};
const Parent = ({ children, onUpdate, onSetPath }) => {
const [{ path, ext }, { setPath }, Provider] = useUseCase(defaultFile, fileUseCase);
const onClick = () => {
setPath(onSetPath?.() || '');
};
useUpdateEffect(() => {
onUpdate?.();
});
return (_jsx(Provider, { children: _jsxs("div", { children: [_jsxs("header", { children: ["parent path: ", path, _jsx("br", {}), "parent ext: ", ext] }), _jsx("main", { children: children }), _jsx("footer", { children: _jsx("button", { onClick: onClick, children: PARENT_BUTTON_TEXT }) })] }) }));
};
const MathParent = ({ extraValue = 0, onUpdate }) => {
const { add } = useUseCase(mathUseCase, { extraValue }, [extraValue]);
const onClick = () => {
add(1, 2);
};
useUpdateEffect(() => {
onUpdate?.();
});
return (_jsx("div", { children: _jsx("button", { onClick: onClick, children: PARENT_BUTTON_TEXT }) }));
};
const useDeepCompareUpdate = (callback, deps) => {
const isFirstRef = useRef(true);
useEffect(() => {
return () => {
isFirstRef.current = false;
};
});
useDeepCompareEffect.call(null, () => {
if (isFirstRef.current) {
return;
}
callback();
}, deps);
};
beforeEach(() => {
subtractionReducer.mockClear();
});
describe('useUseCase', () => {
describe('`useUseCase` should work the same as `useRootCoreCollection`', () => {
test('check context type', () => {
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase);
});
const { current: cores } = result;
expect(Array.isArray(cores)).toBe(true);
expect(cores).toHaveLength(3);
});
test('`entity` should equal `defaultFile`', () => {
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase);
});
const { current: cores } = result;
const [entity] = cores;
expect(entity).toBe(defaultFile);
});
test('Should return the `EntityStore.value` if root entity has not changed', () => {
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase);
});
const { current: collection } = result;
const [, { setPath }] = collection;
act(() => {
setPath(PATH_1);
});
expect(result.current[0]).toEqual({ ...defaultFile, path: PATH_1, ext: EXT_1 });
});
test('Should return the root entity if root entity has changed', () => {
let file = defaultFile;
const { result, rerender } = renderHook(() => {
return useUseCase(file, fileUseCase);
});
file = { ...defaultFile, path: PATH_2 };
rerender();
expect(result.current[0]).toEqual({ ...defaultFile, path: PATH_2 });
// 渲染多一次,避免第二次渲染的 `entity` 没变化而取错缓存的情况
rerender();
expect(result.current[0]).toEqual({ ...defaultFile, path: PATH_2 });
});
test('Should return the root entity if root entity has changed even with entity state changed', () => {
let file = defaultFile;
const { result } = renderHook(() => {
return useUseCase(file, fileUseCase);
});
const { current: collection } = result;
const [, { setPath }] = collection;
act(() => {
file = { ...defaultFile, path: PATH_2 };
setPath(PATH_1);
});
expect(result.current[0]).toEqual({ ...defaultFile, path: PATH_2 });
});
test('`reducers` should be returned as an object', () => {
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase);
});
const { current: cores } = result;
const [, reducers] = cores;
expect(typeof reducers).toBe('object');
});
test('`Provider` should be returned as a function', () => {
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase);
});
const { current: cores } = result;
const [, , Provider] = cores;
expect(typeof Provider).toBe('function');
});
test('`usecase` should be called only once if deps has not provided', () => {
const mockUseCase = jest.fn(fileUseCase);
const { result, rerender } = renderHook(() => {
return useUseCase(defaultFile, mockUseCase);
});
const { current: cores } = result;
const [, { setEntity }] = cores;
expect(mockUseCase).toHaveBeenCalledTimes(1);
act(() => {
setEntity({
...defaultFile,
});
});
rerender();
expect(mockUseCase).toHaveBeenCalledTimes(1);
});
test('`usecase` should be called only once if deps has not changed', () => {
const mockUseCase = jest.fn(fileUseCase);
const { result, rerender } = renderHook(() => {
return useUseCase(defaultFile, mockUseCase, {}, [1, 'x']);
});
const { current: cores } = result;
const [, { setEntity }] = cores;
expect(mockUseCase).toHaveBeenCalledTimes(1);
act(() => {
setEntity({
...defaultFile,
});
});
rerender();
expect(mockUseCase).toHaveBeenCalledTimes(1);
});
test('`usecase` should be called if deps has changed', () => {
let i = 0;
const mockUseCase = jest.fn(fileUseCase);
const { result, rerender } = renderHook(() => {
return useUseCase(defaultFile, mockUseCase, {}, [i++, 'x']);
});
const { current: cores } = result;
const [, { setEntity }] = cores;
expect(mockUseCase).toHaveBeenCalledTimes(1);
act(() => {
setEntity({
...defaultFile,
});
});
expect(mockUseCase).toHaveBeenCalledTimes(2);
rerender();
expect(mockUseCase).toHaveBeenCalledTimes(3);
});
test('after call non-generator reducer, it should not trigger update', () => {
const onUpdate = jest.fn();
const onDeepUpdate = jest.fn();
const { result } = renderHook(() => {
const context = useUseCase(defaultFile, fileUseCase);
const [, reducers] = context;
const { isImage } = reducers;
const update = useUpdate();
useUpdateEffect(() => {
onUpdate();
}, [context]);
useDeepCompareUpdate(() => {
onDeepUpdate();
}, [context]);
return (shouldUpdate) => {
isImage();
if (!shouldUpdate) {
return;
}
update();
};
});
act(() => {
result.current();
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
act(() => {
result.current(true);
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
});
test('after `yield entity`, it should trigger update', () => {
const onUpdate = jest.fn();
const { result } = renderHook(() => {
useUpdateEffect(() => {
onUpdate();
});
return useUseCase(defaultFile, fileUseCase);
});
const { current: cores } = result;
const [entity, reducers] = cores;
const { setPath, setEntity } = reducers;
act(() => {
setPath(PATH_1);
setEntity({ size: 5000 });
});
expect(result.current).not.toBe(cores);
expect(result.current[0]).not.toBe(entity);
expect(result.current[1]).not.toBe(reducers);
expect(result.current[1].setPath).not.toBe(setPath);
expect(result.current[0]).toEqual({
...defaultFile,
path: PATH_1,
ext: EXT_1,
size: 5000,
});
expect(onUpdate).toHaveBeenCalledTimes(1);
});
test('after `yield entity` on async mode, it should trigger update', async () => {
const onUpdate = jest.fn();
const { result } = renderHook(() => {
useUpdateEffect(() => {
onUpdate();
});
return useUseCase(defaultFile, fileUseCase);
});
const { current: cores } = result;
const [entity, reducers] = cores;
const { readFile } = reducers;
const promise = act(() => {
return readFile(PATH_1);
});
expect(result.current).toBe(cores);
expect(result.current).toEqual(cores);
expect(onUpdate).toHaveBeenCalledTimes(0);
await promise;
expect(result.current[0]).not.toBe(entity);
expect(result.current[1]).not.toBe(reducers);
expect(result.current[1].readFile).not.toBe(readFile);
expect(result.current[0].path).toBe(PATH_1);
expect(result.current[0].ext).toBe(EXT_1);
expect(result.current[0].size).toBe(2000);
expect(onUpdate).toHaveBeenCalledTimes(1);
});
test('after `yield entity` callbacks, it should trigger update', () => {
const onUpdate = jest.fn();
const { result } = renderHook(() => {
useUpdateEffect(() => {
onUpdate();
});
return useUseCase(defaultFile, fileUseCase);
});
const { current: cores } = result;
const [, reducers] = cores;
const { init } = reducers;
act(() => {
init({ ...defaultFile, size: 5000 });
});
expect(result.current[0].size).toBe(5000);
expect(result.current[1]).not.toBe(reducers);
expect(result.current[1].init).not.toBe(init);
expect(onUpdate).toHaveBeenCalledTimes(1);
});
test('reducer call should be cached if component is at rendering phase', () => {
const buttonText = 'button';
const addReducer = jest.fn((entity, substring) => {
return entity + substring;
});
const stringUseCase = () => {
const entityReducers = entityUseCase();
return {
...entityReducers,
add: addReducer,
};
};
const CacheTestComponent = () => {
const [, { add }] = useUseCase('', stringUseCase);
const update = useUpdate();
const onClick = () => {
update();
};
add('x');
return _jsx("button", { onClick: onClick, children: buttonText });
};
render(_jsx(CacheTestComponent, {}));
expect(addReducer).toHaveBeenCalledTimes(1);
expect(addReducer).toHaveBeenCalledWith('', 'x');
expect(addReducer).toHaveReturnedWith('x');
fireEvent.click(screen.getByText(buttonText));
expect(addReducer).toHaveBeenCalledTimes(1);
});
test('reducer call should not be cached if component is not at rendering phase', () => {
const buttonText = 'button';
const addReducer = jest.fn((entity, substring) => {
return entity + substring;
});
const stringUseCase = () => {
const entityReducers = entityUseCase();
return {
...entityReducers,
add: addReducer,
};
};
const CacheTestComponent = () => {
const [, { add }] = useUseCase('', stringUseCase);
const onClick = () => {
add('x');
};
useMount(() => {
add('x');
add('x');
});
return _jsx("button", { onClick: onClick, children: buttonText });
};
render(_jsx(CacheTestComponent, {}));
expect(addReducer).toHaveBeenCalledTimes(2);
expect(addReducer).toHaveBeenNthCalledWith(1, '', 'x');
expect(addReducer).toHaveBeenNthCalledWith(2, '', 'x');
fireEvent.click(screen.getByText(buttonText));
expect(addReducer).toHaveBeenCalledTimes(3);
expect(addReducer).toHaveBeenNthCalledWith(3, '', 'x');
});
test('reducer call should not be cached if entity or parameters have changed', () => {
let count = 0;
let addedText = 'x';
const setButtonText = 'button';
const updateButtonText = 'update';
const addReducer = jest.fn((entity, substring) => {
return entity + substring;
});
const stringUseCase = () => {
const entityReducers = entityUseCase();
return {
...entityReducers,
add: addReducer,
};
};
const CacheTestComponent = () => {
const [, { add, setEntity }] = useUseCase('', stringUseCase);
const update = useUpdate();
add(addedText);
const onSet = () => {
setEntity(`${count++}`);
};
const onUpdate = () => {
update();
};
return (_jsxs("div", { children: [_jsx("button", { onClick: onSet, children: setButtonText }), _jsx("button", { onClick: onUpdate, children: updateButtonText })] }));
};
render(_jsx(CacheTestComponent, {}));
expect(addReducer).toHaveBeenCalledTimes(1);
expect(addReducer).toHaveBeenCalledWith('', 'x');
fireEvent.click(screen.getByText(setButtonText));
expect(addReducer).toHaveBeenCalledTimes(2);
expect(addReducer).toHaveBeenCalledWith('0', 'x');
expect(addReducer).toHaveReturnedWith('0x');
addedText = 'y';
fireEvent.click(screen.getByText(updateButtonText));
expect(addReducer).toHaveBeenCalledTimes(3);
expect(addReducer).toHaveBeenCalledWith('0', 'y');
expect(addReducer).toHaveReturnedWith('0y');
fireEvent.click(screen.getByText(updateButtonText));
expect(addReducer).toHaveBeenCalledTimes(3);
});
test('`options.onChange` should be trigger when entity has changed', () => {
const onChange = jest.fn();
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase, { onChange });
});
const { current: cores } = result;
const [, reducers] = cores;
const { setPath } = reducers;
expect(onChange).toHaveBeenCalledTimes(0);
act(() => {
setPath(PATH_1);
});
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenLastCalledWith({ ...defaultFile, path: PATH_1, ext: EXT_1 }, defaultFile);
});
test('`options.onChange` should always trigger the latest one', () => {
const onChange1 = jest.fn();
const onChange2 = jest.fn();
let onChange = onChange1;
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase, { onChange }, []);
});
const { current: cores } = result;
const [, reducers] = cores;
const { setPath } = reducers;
expect(onChange1).toHaveBeenCalledTimes(0);
act(() => {
setPath(PATH_1);
onChange = onChange2;
});
expect(onChange2).toHaveBeenCalledTimes(0);
expect(onChange1).toHaveBeenCalledTimes(1);
expect(onChange1).toHaveBeenLastCalledWith({ ...defaultFile, path: PATH_1, ext: EXT_1 }, defaultFile);
act(() => {
setPath(PATH_2);
});
expect(onChange1).toHaveBeenCalledTimes(1);
expect(onChange2).toHaveBeenCalledTimes(1);
expect(onChange2).toHaveBeenLastCalledWith({ ...defaultFile, path: PATH_2, ext: EXT_2 }, { ...defaultFile, path: PATH_1, ext: EXT_1 });
});
test('`options.options` should override rest options', () => {
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase, {
pathPrefix: 'xyz/',
options: { pathPrefix: '123/' },
});
});
const { current: cores } = result;
const [{ path }, reducers] = cores;
const { setPath } = reducers;
expect(path).toBe('');
act(() => {
setPath(PATH_1);
});
expect(result.current[0].path).toBe('123/' + PATH_1);
});
test('when `options` has changed, it should not trigger update', () => {
const onUpdate = jest.fn();
const onDeepUpdate = jest.fn();
const { result } = renderHook(() => {
const [pathPrefix, setPathPrefix] = useState('user/');
const context = useUseCase(defaultFile, fileUseCase, { pathPrefix });
useUpdateEffect(() => {
onUpdate();
}, [context]);
useDeepCompareUpdate(() => {
onDeepUpdate();
}, [context]);
return setPathPrefix;
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
act(() => {
result.current('my/');
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
});
test('when `deps` has changed, it should trigger update', () => {
const onUpdate = jest.fn();
const onDeepUpdate = jest.fn();
const { result } = renderHook(() => {
const [pathPrefix, setPathPrefix] = useState('user/');
const context = useUseCase(defaultFile, fileUseCase, { pathPrefix }, [pathPrefix]);
useUpdateEffect(() => {
onUpdate();
}, [context]);
useDeepCompareUpdate(() => {
onDeepUpdate();
}, [context]);
return setPathPrefix;
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
act(() => {
result.current('my/');
});
expect(onUpdate).toHaveBeenCalledTimes(1);
expect(onDeepUpdate).toHaveBeenCalledTimes(1);
});
test('empty `deps` should not trigger update', () => {
const onUpdate = jest.fn();
const onDeepUpdate = jest.fn();
const { result } = renderHook(() => {
const [pathPrefix, setPathPrefix] = useState('user/');
const context = useUseCase(defaultFile, fileUseCase, { pathPrefix }, []);
useUpdateEffect(() => {
onUpdate();
}, [context]);
useDeepCompareUpdate(() => {
onDeepUpdate();
}, [context]);
return setPathPrefix;
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
act(() => {
result.current('my/');
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
});
test(`should call latest functions of usecase options even deps has not change`, () => {
const onPathChange1 = jest.fn();
const onPathChange2 = jest.fn();
let onPathChange = onPathChange1;
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase, { onPathChange });
});
const { current: context } = result;
const [, { setPath }] = context;
act(() => {
setPath(PATH_1);
onPathChange = onPathChange2;
});
expect(onPathChange1).toHaveBeenCalledTimes(1);
expect(onPathChange2).toHaveBeenCalledTimes(0);
expect(onPathChange1).toHaveBeenLastCalledWith(PATH_1, '');
act(() => {
setPath(PATH_2);
});
expect(onPathChange1).toHaveBeenCalledTimes(1);
expect(onPathChange2).toHaveBeenCalledTimes(1);
expect(onPathChange2).toHaveBeenLastCalledWith(PATH_2, PATH_1);
});
test('`options.watch` should be trigger after entity changed', () => {
const onPathChange = jest.fn();
const onSizeChange = jest.fn();
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase, {
watch: {
path: onPathChange,
size: onSizeChange,
},
});
});
const { current: cores } = result;
const [, { setPath }] = cores;
expect(onPathChange).toHaveBeenCalledTimes(0);
act(() => {
setPath(PATH_1);
});
expect(onPathChange).toHaveBeenCalledTimes(1);
expect(onSizeChange).toHaveBeenCalledTimes(0);
expect(onPathChange).toHaveBeenLastCalledWith({
fieldPaths: ['path'],
newEntity: { ...defaultFile, path: PATH_1, ext: EXT_1 },
oldEntity: defaultFile,
newValue: PATH_1,
oldValue: '',
});
});
test('`options.watch` should trigger the latest one', () => {
const onPathChange1 = jest.fn();
const onPathChange2 = jest.fn();
let onPathChange = onPathChange1;
const { result } = renderHook(() => {
return useUseCase(defaultFile, fileUseCase, {
watch: {
path: onPathChange,
},
});
});
const { current: cores } = result;
const [, { setPath, setEntity }] = cores;
expect(onPathChange1).toHaveBeenCalledTimes(0);
act(() => {
setPath(PATH_1);
setEntity({ size: 5000 });
onPathChange = onPathChange2;
});
expect(onPathChange1).toHaveBeenCalledTimes(1);
expect(onPathChange1).toHaveBeenLastCalledWith({
fieldPaths: ['path'],
newEntity: { ...defaultFile, path: PATH_1, ext: EXT_1 },
oldEntity: defaultFile,
newValue: PATH_1,
oldValue: '',
});
act(() => {
setPath('');
});
expect(onPathChange1).toHaveBeenCalledTimes(1);
expect(onPathChange2).toHaveBeenCalledTimes(1);
expect(onPathChange2).toHaveBeenLastCalledWith({
fieldPaths: ['path'],
newEntity: { ...defaultFile, size: 5000 },
oldEntity: { ...defaultFile, path: PATH_1, ext: EXT_1, size: 5000 },
newValue: '',
oldValue: PATH_1,
});
});
test('`options.watch` should work with array field path of array', () => {
const initialData = {};
const onExtChange = jest.fn();
const onSizeChange = jest.fn();
const { result } = renderHook(() => {
return useUseCase(initialData, fieldPathUseCase, {
watch: {
'list.ext': onExtChange,
'list.size': onSizeChange,
},
});
});
const { current: cores } = result;
const [, { setEntity }] = cores;
act(() => {
setEntity({ list: [defaultFile] });
});
expect(onExtChange).toHaveBeenCalledTimes(1);
expect(onSizeChange).toHaveBeenCalledTimes(1);
expect(onExtChange).toHaveBeenCalledWith({
fieldPaths: ['list', '0', 'ext'],
newEntity: { list: [defaultFile] },
oldEntity: {},
newValue: '',
oldValue: void 0,
});
expect(onSizeChange).toHaveBeenCalledWith({
fieldPaths: ['list', '0', 'size'],
newEntity: { list: [defaultFile] },
oldEntity: {},
newValue: 0,
oldValue: void 0,
});
act(() => {
setEntity({ list: [{ ...defaultFile, ext: EXT_1 }] });
});
expect(onExtChange).toHaveBeenCalledTimes(2);
expect(onSizeChange).toHaveBeenCalledTimes(1);
expect(onExtChange).toHaveBeenCalledWith({
fieldPaths: ['list', '0', 'ext'],
newEntity: { list: [{ ...defaultFile, ext: EXT_1 }] },
oldEntity: { list: [defaultFile] },
newValue: EXT_1,
oldValue: '',
});
act(() => {
setEntity({
list: [
{ ...defaultFile, ext: EXT_1 },
{ ...defaultFile, ext: EXT_2 },
],
});
});
expect(onExtChange).toHaveBeenCalledTimes(3);
expect(onSizeChange).toHaveBeenCalledTimes(2);
expect(onExtChange).toHaveBeenCalledWith({
fieldPaths: ['list', '1', 'ext'],
newEntity: {
list: [
{ ...defaultFile, ext: EXT_1 },
{ ...defaultFile, ext: EXT_2 },
],
},
oldEntity: { list: [{ ...defaultFile, ext: EXT_1 }] },
newValue: EXT_2,
oldValue: void 0,
});
expect(onSizeChange).toHaveBeenCalledWith({
fieldPaths: ['list', '1', 'size'],
newEntity: {
list: [
{ ...defaultFile, ext: EXT_1 },
{ ...defaultFile, ext: EXT_2 },
],
},
oldEntity: { list: [{ ...defaultFile, ext: EXT_1 }] },
newValue: 0,
oldValue: void 0,
});
act(() => {
setEntity({
nestedList: [],
});
});
expect(onExtChange).toHaveBeenCalledTimes(3);
expect(onSizeChange).toHaveBeenCalledTimes(2);
act(() => {
setEntity(() => {
return {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
};
});
});
expect(onExtChange).toHaveBeenCalledTimes(6);
expect(onSizeChange).toHaveBeenCalledTimes(3);
expect(onExtChange).toHaveBeenNthCalledWith(4, {
fieldPaths: ['list', '0', 'ext'],
newEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
oldEntity: {
list: [
{ ...defaultFile, ext: EXT_1 },
{ ...defaultFile, ext: EXT_2 },
],
nestedList: [],
},
newValue: EXT_2,
oldValue: EXT_1,
});
expect(onExtChange).toHaveBeenNthCalledWith(5, {
fieldPaths: ['list', '1', 'ext'],
newEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
oldEntity: {
list: [
{ ...defaultFile, ext: EXT_1 },
{ ...defaultFile, ext: EXT_2 },
],
nestedList: [],
},
newValue: EXT_1,
oldValue: EXT_2,
});
expect(onExtChange).toHaveBeenNthCalledWith(6, {
fieldPaths: ['list', '2', 'ext'],
newEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
oldEntity: {
list: [
{ ...defaultFile, ext: EXT_1 },
{ ...defaultFile, ext: EXT_2 },
],
nestedList: [],
},
newValue: '',
oldValue: void 0,
});
expect(onSizeChange).toHaveBeenCalledWith({
fieldPaths: ['list', '2', 'size'],
newEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
oldEntity: {
list: [
{ ...defaultFile, ext: EXT_1 },
{ ...defaultFile, ext: EXT_2 },
],
nestedList: [],
},
newValue: 0,
oldValue: void 0,
});
act(() => {
setEntity(() => {
return {};
});
});
expect(onExtChange).toHaveBeenCalledTimes(9);
expect(onSizeChange).toHaveBeenCalledTimes(6);
expect(onExtChange).toHaveBeenNthCalledWith(7, {
fieldPaths: ['list', '0', 'ext'],
newEntity: {},
oldEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
newValue: void 0,
oldValue: EXT_2,
});
expect(onExtChange).toHaveBeenNthCalledWith(8, {
fieldPaths: ['list', '1', 'ext'],
newEntity: {},
oldEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
newValue: void 0,
oldValue: EXT_1,
});
expect(onExtChange).toHaveBeenNthCalledWith(9, {
fieldPaths: ['list', '2', 'ext'],
newEntity: {},
oldEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
newValue: void 0,
oldValue: '',
});
expect(onSizeChange).toHaveBeenNthCalledWith(4, {
fieldPaths: ['list', '0', 'size'],
newEntity: {},
oldEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
newValue: void 0,
oldValue: 0,
});
expect(onSizeChange).toHaveBeenNthCalledWith(5, {
fieldPaths: ['list', '1', 'size'],
newEntity: {},
oldEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
newValue: void 0,
oldValue: 0,
});
expect(onSizeChange).toHaveBeenNthCalledWith(6, {
fieldPaths: ['list', '2', 'size'],
newEntity: {},
oldEntity: {
list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
},
newValue: void 0,
oldValue: 0,
});
});
test('`options.watch` should work with field path of array length', () => {
const onEntityLengthChange = jest.fn();
const onListLengthChange = jest.fn();
const lengthUseCase = () => {
return entityUseCase();
};
const { result } = renderHook(() => {
return useUseCase([], lengthUseCase, {
watch: {
length: onEntityLengthChange,
'list.length': onListLengthChange,
},
});
});
const { current: cores } = result;
const [, { setEntity }] = cores;
expect(onEntityLengthChange).toHaveBeenCalledTimes(0);
expect(onListLengthChange).toHaveBeenCalledTimes(0);
act(() => {
setEntity([{ list: [defaultFile, defaultFile] }]);
});
expect(onEntityLengthChange).toHaveBeenCalledTimes(1);
expect(onListLengthChange).toHaveBeenCalledTimes(1);
expect(onEntityLengthChange).toHaveBeenCalledWith({
fieldPaths: ['length'],
newEntity: [{ list: [defaultFile, defaultFile] }],
oldEntity: [],
newValue: 1,
oldValue: 0,
});
expect(onListLengthChange).toHaveBeenCalledWith({
fieldPaths: ['0', 'list', 'length'],
newEntity: [{ list: [defaultFile, defaultFile] }],
oldEntity: [],
newValue: 2,
oldValue: void 0,
});
});
test('`options.watch` should work with field path of nested array', () => {
const initialData = {};
const onChange = jest.fn();
const { result } = renderHook(() => {
return useUseCase(initialData, fieldPathUseCase, {
watch: {
'nestedList.size': onChange,
},
});
});
const { current: cores } = result;
const [, { setEntity }] = cores;
expect(onChange).toHaveBeenCalledTimes(0);
act(() => {
setEntity({ nestedList: [[[defaultFile]]] });
});
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
fieldPaths: ['nestedList', '0', '0', '0', 'size'],
newEntity: { nestedList: [[[defaultFile]]] },
oldEntity: {},
newValue: 0,
oldValue: void 0,
});
act(() => {
setEntity({
nestedList: [
[
[
{ ...defaultFile, size: 100 },
{ ...defaultFile, size: 200 },
],
],
[[{ ...defaultFile, size: 300 }]],
],
});
});
expect(onChange).toHaveBeenCalledTimes(4);
expect(onChange).toHaveBeenNthCalledWith(2, {
fieldPaths: ['nestedList', '0', '0', '0', 'size'],
newEntity: {
nestedList: [
[
[
{ ...defaultFile, size: 100 },
{ ...defaultFile, size: 200 },
],
],
[[{ ...defaultFile, size: 300 }]],
],
},
oldEntity: { nestedList: [[[defaultFile]]] },
newValue: 100,
oldValue: 0,
});
expect(onChange).toHaveBeenNthCalledWith(3, {
fieldPaths: ['nestedList', '0', '0', '1', 'size'],
newEntity: {
nestedList: [
[
[
{ ...defaultFile, size: 100 },
{ ...defaultFile, size: 200 },
],
],
[[{ ...defaultFile, size: 300 }]],
],
},
oldEntity: { nestedList: [[[defaultFile]]] },
newValue: 200,
oldValue: void 0,
});
expect(onChange).toHaveBeenNthCalledWith(4, {
fieldPaths: ['nestedList', '1', '0', '0', 'size'],
newEntity: {
nestedList: [
[
[
{ ...defaultFile, size: 100 },
{ ...defaultFile, size: 200 },
],
],
[[{ ...defaultFile, size: 300 }]],
],
},
oldEntity: { nestedList: [[[defaultFile]]] },
newValue: 300,
oldValue: void 0,
});
});
test('`options.watch` should work with field path of object', () => {
const initialData = {};
const onExtChange = jest.fn();
const onSizeChange = jest.fn();
const { result } = renderHook(() => {
return useUseCase(initialData, fieldPathUseCase, {
watch: {
'obj.file.ext': onExtChange,
'obj.file.size': onSizeChange,
},
});
});
const { current: cores } = result;
const [, { setEntity }] = cores;
expect(onExtChange).toHaveBeenCalledTimes(0);
act(() => {
setEntity({
obj: {
file: {
ext: EXT_1,
},
},
});
});
expect(onExtChange).toHaveBeenCalledTimes(1);
expect(onSizeChange).toHaveBeenCalledTimes(0);
expect(onExtChange).toHaveBeenCalledWith({
fieldPaths: ['obj', 'file', 'ext'],
newEntity: {
obj: {
file: {
ext: EXT_1,
},
},
},
oldEntity: {},
newValue: EXT_1,
oldValue: void 0,
});
act(() => {
setEntity(() => {
return {};
});
});
expect(onExtChange).toHaveBeenCalledTimes(2);
expect(onSizeChange).toHaveBeenCalledTimes(0);
expect(onExtChange).toHaveBeenCalledWith({
fieldPaths: ['obj', 'file', 'ext'],
newEntity: {},
oldEntity: {
obj: {
file: {
ext: EXT_1,
},
},
},
newValue: void 0,
oldValue: EXT_1,
});
});
test('multiple `Provider` should work', () => {
const onNumberMount = jest.fn();
const onStringMount = jest.fn();
const onBooleanMount = jest.fn();
const numberUseCase = () => {
return entityUseCase();
};
const stringUseCase = () => {
return entityUseCase();
};
const booleanUseCase = () => {
return entityUseCase();
};
const TestChildComponent = () => {
const [numberValue] = useUseCase(numberUseCase);
const [stringValue] = useUseCase(stringUseCase);
const [booleanValue] = useUseCase(booleanUseCase);
useMount(() => {
onNumberMount(numberValue);
onStringMount(stringValue);
onBooleanMount(booleanValue);
});
return null;
};
const TestParentComponent = () => {
const [, , NumberProvider] = useUseCase(11, numberUseCase);
const [, , StringProvider] = useUseCase('hello', stringUseCase);
const [, , BooleanProvider] = useUseCase(true, booleanUseCase);
return (_jsx(NumberProvider, { with: [StringProvider, BooleanProvider], children: _jsx(TestChildComponent, {}) }));
};
render(_jsx(TestParentComponent, {}));
expect(onNumberMount).toHaveBeenCalledWith(11);
expect(onStringMount).toHaveBeenCalledWith('hello');
expect(onBooleanMount).toHaveBeenCalledWith(true);
});
});
describe('`useUseCase` should work the same as `useContextualCoreCollection`', () => {
test('Should trigger Parent & Child to update', () => {
const onParentUpdate = jest.fn();
const onChildUpdate = jest.fn();
const onPathChange = jest.fn();
const onUndefinedEntity = jest.fn();
const onSetPath = jest.fn(() => {
return PATH_1;
});
render(_jsx("div", { children: _jsx(Parent, { onSetPath: onSetPath, onUpdate: onParentUpdate, children: _jsx(Child, { onUpdate: onChildUpdate, onPathChange: onPathChange, onUndefinedEntity: onUndefinedEntity }) }) }));
fireEvent.click(screen.getByText(PARENT_BUTTON_TEXT));
expect(onSetPath).toHaveBeenCalledTimes(1);
expect(onParentUpdate).toHaveBeenCalledTimes(1);
expect(onChildUpdate).toHaveBeenCalledTimes(1);
expect(onPathChange).toHaveBeenCalledWith(PATH_1);
fireEvent.click(screen.getByText(CHILD_BUTTON_TEXT));
expect(onSetPath).toHaveBeenCalledTimes(1);
expect(onParentUpdate).toHaveBeenCalledTimes(2);
expect(onChildUpdate).toHaveBeenCalledTimes(2);
expect(onPathChange).toHaveBeenCalledWith(PATH_2);
expect(onUndefinedEntity).toHaveBeenCalledTimes(0);
});
test('`usecase` should not be called at child component', () => {
const mockUseCase = jest.fn(fileUseCase);
const A = () => {
useUseCase(mockUseCase);
return _jsx(Fragment, {});
};
const B = () => {
const [, , Provider] = useUseCase(defaultFile, mockUseCase);
return (_jsx(Provider, { children: _jsx(A, {}) }));
};
render(_jsx(B, {}));
expect(mockUseCase).toHaveBeenCalledTimes(1);
});
test('reducer call should be cached if component is at rendering phase in child components', () => {
const buttonText = 'button';
const addReducer = jest.fn((entity, substring) => {
return entity + substring;
});
const stringUseCase = () => {
const entityReducers = entityUseCase();
return {
...entityReducers,
add: addReducer,
};
};
const A = () => {
const [, { add }] = useUseCase(stringUseCase);
const update = useUpdate();
const onClick = () => {
update();
};
add('x');
return _jsx("button", { onClick: onClick, children: buttonText });
};
const B = () => {
const [, , Provider] = useUseCase('', stringUseCase);
return (_jsx(Provider, { children: _jsx(A, {}) }));
};
render(_jsx(B, {}));
expect(addReducer).toHaveBeenCalledTimes(1);
expect(addReducer).toHaveBeenCalledWith('', 'x');
expect(addReducer).toHaveReturnedWith('x');
fireEvent.click(screen.getByText(buttonText));
expect(addReducer).toHaveBeenCalledTimes(1);
});
test('reducer call should not be cached if component is not at rendering phase in child components', () => {
const buttonText = 'button';
const addReducer = jest.fn((entity, substring) => {
return entity + substring;
});
const stringUseCase = () => {
const entityReducers = entityUseCase();
return {
...entityReducers,
add: addReducer,
};
};
const A = () => {
const [, { add }] = useUseCase(stringUseCase);
const onClick = () => {
add('x');
};
useMount(() => {
add('x');
add('x');
});
return _jsx("button", { onClick: onClick, children: buttonText });
};
const B = () => {
const [, , Provider] = useUseCase('', stringUseCase);
return (_jsx(Provider, { children: _jsx(A, {}) }));
};
render(_jsx(B, {}));
expect(addReducer).toHaveBeenCalledTimes(2);
expect(addReducer).toHaveBeenNthCalledWith(1, '', 'x');
expect(addReducer).toHaveBeenNthCalledWith(2, '', 'x');
fireEvent.click(screen.getByText(buttonText));
expect(addReducer).toHaveBeenCalledTimes(3);
expect(addReducer).toHaveBeenNthCalledWith(3, '', 'x');
});
test('reducer call should not be cached if entity or parameters have changed in child components', () => {
let count = 0;
let addedText = 'x';
const setButtonText = 'button';
const updateButtonText = 'update';
const addReducer = jest.fn((entity, substring) => {
return entity + substring;
});
const stringUseCase = () => {
const entityReducers = entityUseCase();
return {
...entityReducers,
add: addReducer,
};
};
const A = () => {
const [, { add, setEntity }] = useUseCase(stringUseCase);
const update = useUpdate();
add(addedText);
const onSet = () => {
setEntity(`${count++}`);
};
const onUpdate = () => {
update();
};
return (_jsxs("div", { children: [_jsx("button", { onClick: onSet, children: setButtonText }), _jsx("button", { onClick: onUpdate, children: updateButtonText })] }));
};
const B = () => {
const [, , Provider] = useUseCase('', stringUseCase);
return (_jsx(Provider, { children: _jsx(A, {}) }));
};
render(_jsx(B, {}));
expect(addReducer).toHaveBeenCalledTimes(1);
expect(addReducer).toHaveBeenCalledWith('', 'x');
fireEvent.click(screen.getByText(setButtonText));
expect(addReducer).toHaveBeenCalledTimes(2);
expect(addReducer).toHaveBeenCalledWith('0', 'x');
expect(addReducer).toHaveReturnedWith('0x');
addedText = 'y';
fireEvent.click(screen.getByText(updateButtonText));
expect(addReducer).toHaveBeenCalledTimes(3);
expect(addReducer).toHaveBeenCalledWith('0', 'y');
expect(addReducer).toHaveReturnedWith('0y');
fireEvent.click(screen.getByText(updateButtonText));
expect(addReducer).toHaveBeenCalledTimes(3);
});
test('`options.onChange` event should be trigger in orders', () => {
let changeTimes = 0;
const updateChangeTimes = () => {
return ++changeTimes;
};
const onChangeA = jest.fn(updateChangeTimes);
const onChangeB = jest.fn(updateChangeTimes);
const onChangeC = jest.fn(updateChangeTimes);
const buttonText = 'my button';
const A = ({ children }) => {
useUseCase(fileUseCase, { onChange: onChangeA });
return _jsx(Fragment, { children: children });
};
const B = () => {
useUseCase(fileUseCase, { onChange: onChangeB });
return _jsx(Fragment, {});
};
const C = () => {
const [, { setPath }, Provider] = useUseCase(defaultFile, fileUseCase, {
onChange: onChangeC,
});
const onFirstClick = () => {
setPath(PATH_1);
};
return (_jsxs(Provider, { children: [_jsx(A, { children: _jsx(B, {}) }), _jsx("button", { onClick: onFirstClick, children: buttonText })] }));
};
render(_jsx(C, {}));
expect(changeTimes).toBe(0);
fireEvent.click(screen.getByText(buttonText));
expect(onChangeA).toHaveReturnedWith(2);
expect(onChangeB).toHaveReturnedWith(1);
expect(onChangeC).toHaveReturnedWith(3);
});
test('`options.onChange` should always trigger the latest one in child components', () => {
const onFirstChange = jest.fn();
const onSecondChange = jest.fn();
let onChange = onFirstChange;
const firstButtonText = 'first button';
const secondButtonText = 'second button';
const A = () => {
const [, { setPath }] = useUseCase(fileUseCase, {
onChange,
});
const onFirstClick = () => {
setPath(PATH_1);
onChange = onSecondChange;
};
const onSecondClick = () => {
setPath('');
};
return (_jsxs(Fragment, { children: [_jsx("button", { onClick: onFirstClick, children: firstButtonText }), _jsx("button", { onClick: onSecondClick, children: secondButtonText })] }));
};
const B = () => {
const [, , Provider] = useUseCase(defaultFile, fileUseCase);
return (_jsx(Provider, { children: _jsx(A, {}) }));
};
render(_jsx(B, {}));
expect(onFirstChange).toHaveBeenCalledTimes(0);
fireEvent.click(screen.getByText(firstButtonText));
expect(onFirstChange).toHaveBeenCalledTimes(1);
expect(onSecondChange).toHaveBeenCalledTimes(0);
fireEvent.click(screen.getByText(secondButtonText));
expect(onFirstChange).toHaveBeenCalledTimes(1);
expect(onSecondChange).toHaveBeenCalledTimes(1);
expect(onSecondChange).toHaveBeenLastCalledWith(defaultFile, { ...defaultFile, ext: EXT_1, path: PATH_1 });
});
test('`options.watch` should be trigger at child elements', () => {
const onExtChange = jest.fn();
const firstButtonText = 'first button';
const A = () => {
const [, { setPath }] = useUseCase(fileUseCase, {
watch: {
ext: onExtChange,
},
});
const onFirstClick = () => {
setPath(PATH_1);
};
return _jsx("button", { onClick: onFirstClick, children: firstButtonText });
};
const B = () => {
const [, , Provider] = useUseCase(defaultFile, fileUseCase, {});
return (_jsx(Provider, { children: _jsx(A, {}) }));
};
render(_jsx(B, {}));
expect(onExtChange).toHaveBeenCalledTimes(0);
fireEvent.click(screen.getByText(firstButtonText));
expect(onExtChange).toHaveBeenCalledTimes(1);
expect(onExtChange).toHaveBeenLastCalledWith({
fieldPaths: ['ext'],
newEntity: { ...defaultFile, ext: EXT_1, path: PATH_1 },
oldEntity: defaultFile,
newValue: EXT_1,
oldValue: '',
});
});
test('`options.watch` should be trigger in orders', () => {
let changeTimes = 0;
const updateChangeTimes = (e) => {
void e;
return ++changeTimes;
};
const onChangeA = jest.fn(updateChangeTimes);
const onChangeB = jest.fn(updateChangeTimes);
const onChangeC = jest.fn(updateChangeTimes);
const buttonText = 'my button';
const A = ({ children }) => {
useUseCase(fileUseCase, {
watch: {
ext: onChangeA,
},
});
return _jsx(Fragment, { children: children });
};
const B = () => {
useUseCase(fileUseCase, {
watch: {
ext: onChangeB,
},
});
return _jsx(Fragment, {});
};
const C = () => {
const [, { setPath }, Provider] = useUseCase(defaultFile, fileUseCase, {
watch: {
ext: onChangeC,
},
});
const onFirstClick = () => {
setPath(PATH_1);
};
return (_jsxs(Provider, { children: [_jsx(A, { children: _jsx(B, {}) }), _jsx("button", { onClick: onFirstClick, children: buttonText })] }));
};
render(_jsx(C, {}));
expect(changeTimes).toBe(0);
fireEvent.click(screen.getByText(buttonText));
expect(onChangeA).toHaveReturnedWith(2);
expect(onChangeB).toHaveReturnedWith(1);
expect(onChangeC).toHaveReturnedWith(3);
});
test('`options.watch` should trigger the latest one in child components', () => {
const onFirstExtChange = jest.fn();
const onSecondExtChange = jest.fn();
let onExtChange = onFirstExtChange;
const firstButtonText = 'first button';
const secondButtonText = 'second button';
const A = () => {
const [, { setPath }] = useUseCase(fileUseCase, {
watch: {
ext: onExtChange,
},
});
const onFirstClick = () => {
setPath(PATH_1);
onExtChange = onSecondExtChange;
};
const onSecondClick = () => {
setPath('');
};
return (_jsxs(Fragment, { children: [_jsx("button", { onClick: onFirstClick, children: firstButtonText }), _jsx("button", { onClick: onSecondClick, children: secondButtonText })] }));
};
const B = () => {
const [, , Provider] = useUseCase(defaultFile, fileUseCase);
return (_jsx(Provider, { children: _jsx(A, {}) }));
};
render(_jsx(B, {}));
expect(onFirstExtChange).toHaveBeenCalledTimes(0);
fireEvent.click(screen.getByText(firstButtonText));
expect(onFirstExtChange).toHaveBeenCalledTimes(1);
expect(onSecondExtChange).toHaveBeenCalledTimes(0);
fireEvent.click(screen.getByText(secondButtonText));
expect(onFirstExtChange).toHaveBeenCalledTimes(1);
expect(onSecondExtChange).toHaveBeenCalledTimes(1);
expect(onSecondExtChange).toHaveBeenLastCalledWith({
fieldPaths: ['ext'],
newEntity: defaultFile,
oldEntity: { ...defaultFile, ext: EXT_1, path: PATH_1 },
newValue: '',
oldValue: EXT_1,
});
});
});
describe('`useUseCase` should work the same as `useReducers`', () => {
test('`entity` should equal `defaultFile`', () => {
const { result } = renderHook(() => {
return useUseCase(mathUseCase);
});
const { current: reducers } = result;
expect(Array.isArray(reducers)).toBe(false);
});
test('returned reducer should work', () => {
const { result } = renderHook(() => {
return useUseCase(mathUseCase);
});
const { current: reducers } = result;
const { add } = reducers;
expect(add(1, 2)).toBe(3);
});
test('`usecase` should be called only once if deps has not provided', () => {
const mockUseCase = jest.fn(mathUseCase);
const { rerender } = renderHook(() => {
return useUseCase(mockUseCase);
});
expect(mockUseCase).toHaveBeenCalledTimes(1);
rerender();
expect(mockUseCase).toHaveBeenCalledTimes(1);
});
test('`usecase` should be called only once if deps has not changed', () => {
const mockUseCase = jest.fn(mathUseCase);
const { rerender } = renderHook(() => {
return useUseCase(mockUseCase, {}, [1, 'x']);
});
expect(mockUseCase).toHaveBeenCalledTimes(1);
rerender();
expect(mockUseCase).toHaveBeenCalledTimes(1);
});
test('`usecase` should be called if deps has changed', () => {
let i = 0;
const mockUseCase = jest.fn(mathUseCase);
const { rerender } = renderHook(() => {
return useUseCase(mockUseCase, {}, [i++, 'x']);
});
expect(mockUseCase).toHaveBeenCalledTimes(1);
rerender();
expect(mockUseCase).toHaveBeenCalledTimes(2);
});
test('reducer call should not be cached if component is at rendering phase', () => {
const buttonText = 'button';
const CacheTestComponent = () => {
const { subtraction } = useUseCase(mathUseCase);
const update = useUpdate();
const onClick = () => {
update();
};
subtraction(5, 3);
return _jsx("button", { onClick: onClick, children: buttonText });
};
render(_jsx(CacheTestComponent, {}));
expect(subtractionReducer).toHaveBeenCalledTimes(1);
expect(subtractionReducer).toHaveBeenCalledWith(5, 3);
expect(subtractionReducer).toHaveReturnedWith(2);
fireEvent.click(screen.getByText(buttonText));
expect(subtractionReducer).toHaveBeenCalledTimes(2);
fireEvent.click(screen.getByText(buttonText));
expect(subtractionReducer).toHaveBeenCalledTimes(3);
});
test('reducer call should not be cached if component is not at rendering phase', () => {
const buttonText = 'button';
const CacheTestComponent = () => {
const { subtraction } = useUseCase(mathUseCase);
const onClick = () => {
subtraction(5, 3);
};
useMount(() => {
subtraction(5, 3);
subtraction(5, 3);
});
return _jsx("button", { onClick: onClick, children: buttonText });
};
render(_jsx(CacheTestComponent, {}));
expect(subtractionReducer).toHaveBeenCalledTimes(2);
expect(subtractionReducer).toHaveBeenNthCalledWith(1, 5, 3);
expect(subtractionReducer).toHaveNthReturnedWith(1, 2);
expect(subtractionReducer).toHaveBeenNthCalledWith(2, 5, 3);
expect(subtractionReducer).toHaveNthReturnedWith(2, 2);
fireEvent.click(screen.getByText(buttonText));
expect(subtractionReducer).toHaveBeenCalledTimes(3);
expect(subtractionReducer).toHaveBeenNthCalledWith(3, 5, 3);
expect(subtractionReducer).toHaveNthReturnedWith(3, 2);
});
test('reducer call should not be cached if parameters have changed', () => {
let count = 0;
const buttonText = 'button';
const StringUseCaseTestComponent = () => {
const { subtraction } = useUseCase(mathUseCase);
const update = useUpdate();
subtraction(5, count++);
const onUpdate = () => {
update();
};
return (_jsx("div", { children: _jsx("button", { onClick: onUpdate, children: buttonText }) }));
};
render(_jsx(StringUseCaseTestComponent, {}));
expect(subtractionReducer).toHaveBeenCalledTimes(1);
expect(subtractionReducer).toHaveBeenCalledWith(5, 0);
fireEvent.click(screen.getByText(buttonText));
expect(subtractionReducer).toHaveBeenCalledTimes(2);
expect(subtractionReducer).toHaveBeenCalledWith(5, 1);
expect(subtractionReducer).toHaveReturnedWith(4);
});
test('`options.extraValue` should be added after call `add` method', () => {
const { result } = renderHook(() => {
return useUseCase(mathUseCase, { extraValue: 5 });
});
const { current: reducers } = result;
const { add } = reducers;
expect(add(1, 2)).toBe(8);
});
test('when `options` has changed, it should not trigger update', () => {
const onUpdate = jest.fn();
const onDeepUpdate = jest.fn();
const { result } = renderHook(() => {
const [extraValue, setExtraValue] = useState(0);
const reducers = useUseCase(mathUseCase, { extraValue });
useUpdateEffect(() => {
onUpdate();
}, [reducers]);
useDeepCompareUpdate(() => {
onDeepUpdate();
}, [reducers]);
return setExtraValue;
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
act(() => {
result.current(3);
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
});
test('when `deps` change, it should update reducers', () => {
const onUpdate = jest.fn();
const onDeepUpdate = jest.fn();
const { result } = renderHook(() => {
const [extraValue, setExtraValue] = useState(0);
const reducers = useUseCase(mathUseCase, { extraValue }, [extraValue]);
useUpdateEffect(() => {
onUpdate();
}, [reducers]);
useDeepCompareUpdate(() => {
onDeepUpdate();
}, [reducers]);
return setExtraValue;
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
act(() => {
result.current(3);
});
expect(onUpdate).toHaveBeenCalledTimes(1);
expect(onDeepUpdate).toHaveBeenCalledTimes(1);
});
test('empty `deps` should not update reducers', () => {
const onUpdate = jest.fn();
const onDeepUpdate = jest.fn();
const { result } = renderHook(() => {
const [extraValue, setExtraValue] = useState(0);
const reducers = useUseCase(mathUseCase, { extraValue }, []);
useUpdateEffect(() => {
onUpdate();
}, [reducers]);
useDeepCompareUpdate(() => {
onDeepUpdate();
}, [reducers]);
return setExtraValue;
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
act(() => {
result.current(3);
});
expect(onUpdate).toHaveBeenCalledTimes(0);
expect(onDeepUpdate).toHaveBeenCalledTimes(0);
});
test('should not trigger update', () => {
const onUpdate = jest.fn();
render(_jsx(MathParent, { onUpdate: onUpdate }));
fireEvent.click(screen.getByText(PARENT_BUTTON_TEXT));
expect(onUpdate).toHaveBeenCalledTimes(0);
});
});
});