Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / redux-utilities / asyncModelThunk / hooks.test.tsx
blobba9eb46b89386d7f804c8e951f3f84d321c078c7
1 import type { ReactNode } from 'react';
2 import { Provider } from 'react-redux';
4 import { describe, expect, test } from '@jest/globals';
5 import { type Action, configureStore, createSlice } from '@reduxjs/toolkit';
6 import { act, render, waitFor } from '@testing-library/react';
7 import type { Store } from 'redux';
9 import { ProtonStoreContext } from '@proton/react-redux-store';
11 import { createAsyncModelThunk, handleAsyncModel } from './creator';
12 import { ModelThunkDispatcher, createHooks } from './hooks';
14 export interface ProtonStoreProviderProps<S = any, A extends Action = Action> {
15     store: Store<S, A>;
16     children: ReactNode;
19 const ProtonStoreProvider = ({ children, store }: ProtonStoreProviderProps) => {
20     return (
21         <Provider context={ProtonStoreContext} store={store}>
22             <ModelThunkDispatcher>{children}</ModelThunkDispatcher>
23         </Provider>
24     );
27 interface ModelState<T> {
28     value: T | undefined;
29     error: any;
30     meta: {
31         fetchedAt: number;
32         fetchedEphemeral: boolean | undefined;
33     };
36 describe('hooks', () => {
37     interface StoreState<T> {
38         myState: ModelState<T>;
39     }
41     const stateKey = 'myState';
43     const selectState = <T,>(state: StoreState<T>) => state[stateKey];
45     const setup = <T,>(miss: () => Promise<T>) => {
46         const initialState: ModelState<T> = {
47             value: undefined,
48             error: undefined,
49             meta: {
50                 fetchedAt: 0,
51                 fetchedEphemeral: undefined,
52             },
53         };
55         const thunkActionCreator = createAsyncModelThunk<T, StoreState<T>, undefined>('myState/fetch', {
56             miss,
57             previous: (store) => {
58                 return selectState(store.getState());
59             },
60         });
62         const slice = createSlice({
63             name: 'myState',
64             initialState,
65             reducers: {
66                 reset: (state) => {
67                     state.value = undefined;
68                     state.error = undefined;
69                 },
70             },
71             extraReducers: (builder) => {
72                 handleAsyncModel(builder, thunkActionCreator);
73             },
74         });
76         const getNewStore = () =>
77             configureStore({
78                 reducer: {
79                     [stateKey]: slice.reducer,
80                 },
81             });
83         return {
84             slice,
85             getNewStore,
86             thunkActionCreator,
87         };
88     };
90     test('successfully selects a value', async () => {
91         const { getNewStore, thunkActionCreator } = setup(async () => 42);
92         const store = getNewStore();
93         const hooks = createHooks(thunkActionCreator.thunk, selectState);
95         const Component = () => {
96             const [value, loading] = hooks.useValue();
97             return <div data-testid="result">{`${value}, ${loading}`}</div>;
98         };
100         const { getByTestId } = render(
101             <ProtonStoreProvider store={store}>
102                 <Component />
103             </ProtonStoreProvider>
104         );
106         const div = getByTestId('result');
107         expect(div).toHaveTextContent('undefined, true');
108         await waitFor(() => expect(div).toHaveTextContent('42, false'));
109     });
111     test('only dispatches on undefined values', async () => {
112         const { getNewStore, thunkActionCreator } = setup(async () => 42);
113         const store = getNewStore();
114         const spy = jest.spyOn(thunkActionCreator, 'thunk');
115         const hooks = createHooks(thunkActionCreator.thunk, selectState);
117         const Component = () => {
118             const [value, loading] = hooks.useValue();
119             return <div data-testid="result">{`${value}, ${loading}`}</div>;
120         };
122         const { getByTestId } = render(
123             <ProtonStoreProvider store={store}>
124                 <Component />
125             </ProtonStoreProvider>
126         );
128         expect(spy).toHaveBeenCalledTimes(0);
129         const div = getByTestId('result');
130         expect(div).toHaveTextContent('undefined, true');
132         await waitFor(() => expect(div).toHaveTextContent('42, false'));
134         expect(spy).toHaveBeenCalledTimes(1);
136         render(
137             <ProtonStoreProvider store={store}>
138                 <Component />
139             </ProtonStoreProvider>
140         );
142         expect(spy).toHaveBeenCalledTimes(1);
143     });
145     test('re-dispatches when value becomes undefined', async () => {
146         const { getNewStore, thunkActionCreator, slice } = setup(async () => 42);
147         const store = getNewStore();
148         const spy = jest.spyOn(thunkActionCreator, 'thunk');
149         const hooks = createHooks(thunkActionCreator.thunk, selectState);
151         const Component = () => {
152             const [value, loading] = hooks.useValue();
153             return <div data-testid="result">{`${value}, ${loading}`}</div>;
154         };
156         const { getByTestId } = render(
157             <ProtonStoreProvider store={store}>
158                 <Component />
159             </ProtonStoreProvider>
160         );
162         expect(spy).toHaveBeenCalledTimes(0);
164         const div = getByTestId('result');
165         expect(div).toHaveTextContent('undefined, true');
166         await waitFor(() => expect(div).toHaveTextContent('42, false'));
168         expect(spy).toHaveBeenCalledTimes(1);
170         render(
171             <ProtonStoreProvider store={store}>
172                 <Component />
173             </ProtonStoreProvider>
174         );
176         expect(spy).toHaveBeenCalledTimes(1);
178         act(() => {
179             store.dispatch(slice.actions.reset());
180         });
182         render(
183             <ProtonStoreProvider store={store}>
184                 <Component />
185             </ProtonStoreProvider>
186         );
188         expect(spy).toHaveBeenCalledTimes(1);
189         expect(div).toHaveTextContent('undefined, true');
190         await waitFor(() => expect(div).toHaveTextContent('42, false'));
192         expect(spy).toHaveBeenCalledTimes(2);
194         render(
195             <ProtonStoreProvider store={store}>
196                 <Component />
197             </ProtonStoreProvider>
198         );
200         expect(spy).toHaveBeenCalledTimes(2);
201     });