UNPKG

@mic-rexjs/usecases-react

Version:

React-based solution for use usecases of Clean Architecture

1,627 lines 69.4 kB
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);
        });
    });
});