diff --git a/sources/client/src/components/composite-entities-by-kind.tsx b/sources/client/src/components/composite-entities-by-kind.tsx index b2e0f1b..772c3b3 100644 --- a/sources/client/src/components/composite-entities-by-kind.tsx +++ b/sources/client/src/components/composite-entities-by-kind.tsx @@ -12,7 +12,7 @@ import { Set } from '../vo/set'; export function CompositeEntitiesByKind( props: EntitiesSearch.CompositeEntitiesKinds ): JSX.Element { - const { state, dispatch } = useEntitiesOptionsStorage( + const [state, dispatch] = useEntitiesOptionsStorage( { entities: props.entities.value, kind: props.kind.value, diff --git a/sources/client/src/hooks/use-entities-options-storage.ts b/sources/client/src/hooks/use-entities-options-storage.ts index 96e8b55..62bcb55 100644 --- a/sources/client/src/hooks/use-entities-options-storage.ts +++ b/sources/client/src/hooks/use-entities-options-storage.ts @@ -15,10 +15,12 @@ type _Reducer = Reducer< export function useEntitiesOptionsStorage( initialState: Partial>, searchEntities: EntitiesSearch.SearchEntitiesFunction -): Readonly<{ - state: EntitiesSearch.EntitiesState; - dispatch: Dispatch>; -}> { +): Readonly< + [ + EntitiesSearch.EntitiesState, + Dispatch> + ] +> { const [state, dispatch] = useReducer<_Reducer>( reducer, makeInitialState(initialState) @@ -62,8 +64,5 @@ export function useEntitiesOptionsStorage( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return { - state, - dispatch, - }; + return [state, dispatch] as const; } diff --git a/tests/client/unit/hooks/use-entities-options-storage.test.ts b/tests/client/unit/hooks/use-entities-options-storage.test.ts deleted file mode 100644 index 6ac9572..0000000 --- a/tests/client/unit/hooks/use-entities-options-storage.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it, jest } from '@jest/globals'; - -import { renderHook } from '@testing-library/react'; - -import { useEntitiesOptionsStorage } from '../../../../sources/client/src/hooks/use-entities-options-storage'; - -jest.mock('react', () => ({ - // @ts-ignore - ...jest.requireActual('react'), - useEffect: jest.fn(), -})); -describe('Use Posts Options Storage', () => { - it('returns always the same state and dispatcher', () => { - const entitiesSearch = jest.fn() as Parameters< - typeof useEntitiesOptionsStorage - >[1]; - - // @ts-ignore - const { state, dispatch } = renderHook(() => - useEntitiesOptionsStorage({}, entitiesSearch) - ); - // @ts-ignore - const { state: state2, dispatch: dispatch2 } = renderHook(() => - useEntitiesOptionsStorage({}, entitiesSearch) - ); - - expect(state === state2).toEqual(true); - expect(dispatch === dispatch2).toEqual(true); - }); -}); diff --git a/tests/client/unit/hooks/use-entities-options-storage.test.tsx b/tests/client/unit/hooks/use-entities-options-storage.test.tsx new file mode 100644 index 0000000..376771d --- /dev/null +++ b/tests/client/unit/hooks/use-entities-options-storage.test.tsx @@ -0,0 +1,241 @@ +import EntitiesSearch from '@types'; +import React from 'react'; + +import { describe, expect, it, jest } from '@jest/globals'; + +import { act, render } from '@testing-library/react'; + +import { doAction } from '@wordpress/hooks'; + +import { useEntitiesOptionsStorage } from '../../../../sources/client/src/hooks/use-entities-options-storage'; +import { Set } from '../../../../sources/client/src/vo/set'; + +jest.mock('@wordpress/hooks', () => ({ + doAction: jest.fn(), +})); + +describe('Use Posts Options Storage', () => { + it('Ensure seachEntities is called with the right data on state hydratation.', async () => { + const kind = new Set(['post']); + const entities = new Set([1, 2, 3]); + const searchEntities = jest.fn(() => + Promise.resolve( + new Set([ + { + label: 'post-title', + value: 1, + }, + ]) + ) + ) as jest.Mock>; + + const Component = () => { + useEntitiesOptionsStorage( + { + kind, + entities, + }, + searchEntities + ); + + return null; + }; + + await act(() => render()); + + expect(searchEntities).toHaveBeenCalledWith('', kind, { + exclude: entities, + }); + expect(searchEntities).toHaveBeenCalledWith('', kind, { + include: entities, + per_page: '-1', + }); + }); + + it('Update the state based on the given kind and entities', async () => { + const kind = new Set(['post']); + const entities = new Set([1, 2, 3]); + const selectedEntitiesOptions = new Set([ + { + label: 'post-title-1', + value: 1, + }, + { + label: 'post-title-2', + value: 2, + }, + { + label: 'post-title-3', + value: 3, + }, + ]); + const currentEntitiesOptions = new Set([ + { + label: 'post-title-4', + value: 4, + }, + { + label: 'post-title-5', + value: 5, + }, + { + label: 'post-title-6', + value: 6, + }, + ]); + + const searchEntities = jest.fn((_phrase, _kind, options) => { + if (options?.include) { + return Promise.resolve(selectedEntitiesOptions); + } + + return options?.include + ? Promise.resolve(selectedEntitiesOptions) + : Promise.resolve(currentEntitiesOptions); + }) as jest.Mock>; + + const dispatch = jest.fn(); + jest.spyOn(React, 'useReducer').mockImplementation((_, state) => [ + state, + dispatch, + ]); + + const Component = () => { + useEntitiesOptionsStorage( + { + kind, + entities, + }, + searchEntities + ); + + return null; + }; + + await act(() => render()); + + expect(dispatch).toHaveBeenCalledWith({ + type: 'UPDATE_SELECTED_ENTITIES_OPTIONS', + selectedEntitiesOptions, + }); + expect(dispatch).toHaveBeenCalledWith({ + type: 'UPDATE_CONTEXTUAL_ENTITIES_OPTIONS', + contextualEntitiesOptions: currentEntitiesOptions, + }); + expect(dispatch).toHaveBeenCalledWith({ + type: 'UPDATE_CURRENT_ENTITIES_OPTIONS', + currentEntitiesOptions, + }); + }); + + it('Sete the current and selected entities options to an empty set if searchEntities fails', async () => { + const kind = new Set(['post']); + const entities = new Set([1, 2, 3]); + const searchEntities = jest.fn(() => + Promise.resolve(null) + ) as jest.Mock<() => Promise>; + + const dispatch = jest.fn(); + jest.spyOn(React, 'useReducer').mockImplementation((_, state) => [ + state, + dispatch, + ]); + + const Component = () => { + useEntitiesOptionsStorage( + { + kind, + entities, + }, + // @ts-ignore + searchEntities + ); + + return null; + }; + + await act(() => render()); + + const expectedSet = new Set(); + + expect(dispatch).toHaveBeenCalledWith({ + type: 'UPDATE_SELECTED_ENTITIES_OPTIONS', + selectedEntitiesOptions: expectedSet, + }); + expect(dispatch).toHaveBeenCalledWith({ + type: 'UPDATE_CONTEXTUAL_ENTITIES_OPTIONS', + contextualEntitiesOptions: expectedSet, + }); + expect(dispatch).toHaveBeenCalledWith({ + type: 'UPDATE_CURRENT_ENTITIES_OPTIONS', + currentEntitiesOptions: expectedSet, + }); + }); + + it('Does not call searchEntities with includes if entities is empty', async () => { + const kind = new Set(['post']); + const entities = new Set(); + const searchEntities = jest.fn(() => + Promise.resolve( + new Set([ + { + label: 'post-title', + value: 1, + }, + ]) + ) + ) as jest.Mock>; + + const Component = () => { + useEntitiesOptionsStorage( + { + kind, + // @ts-ignore + entities, + }, + searchEntities + ); + + return null; + }; + + await act(() => render()); + + expect(searchEntities).toHaveBeenCalledTimes(1); + expect(searchEntities).toHaveBeenCalledWith('', kind, { + exclude: entities, + }); + expect(searchEntities).not.toHaveBeenCalledWith('', kind, { + include: entities, + per_page: '-1', + }); + }); + + it('Execute the action wp-entities-search.on-storage-initialization.error when there is an error on searchEntites', async () => { + const kind = new Set(['post']); + const entities = new Set([1, 2, 3]); + const searchEntities = jest.fn(() => + Promise.reject('Search Entities Failed.') + ); + + const Component = () => { + useEntitiesOptionsStorage( + { + kind, + entities, + }, + // @ts-ignore + searchEntities + ); + + return null; + }; + + await act(() => render()); + + expect(doAction).toHaveBeenCalledWith( + 'wp-entities-search.on-storage-initialization.error', + 'Search Entities Failed.' + ); + }); +});