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> {
19 const ProtonStoreProvider = ({ children, store }: ProtonStoreProviderProps) => {
21 <Provider context={ProtonStoreContext} store={store}>
22 <ModelThunkDispatcher>{children}</ModelThunkDispatcher>
27 interface ModelState<T> {
32 fetchedEphemeral: boolean | undefined;
36 describe('hooks', () => {
37 interface StoreState<T> {
38 myState: ModelState<T>;
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> = {
51 fetchedEphemeral: undefined,
55 const thunkActionCreator = createAsyncModelThunk<T, StoreState<T>, undefined>('myState/fetch', {
57 previous: (store) => {
58 return selectState(store.getState());
62 const slice = createSlice({
67 state.value = undefined;
68 state.error = undefined;
71 extraReducers: (builder) => {
72 handleAsyncModel(builder, thunkActionCreator);
76 const getNewStore = () =>
79 [stateKey]: slice.reducer,
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>;
100 const { getByTestId } = render(
101 <ProtonStoreProvider store={store}>
103 </ProtonStoreProvider>
106 const div = getByTestId('result');
107 expect(div).toHaveTextContent('undefined, true');
108 await waitFor(() => expect(div).toHaveTextContent('42, false'));
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>;
122 const { getByTestId } = render(
123 <ProtonStoreProvider store={store}>
125 </ProtonStoreProvider>
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);
137 <ProtonStoreProvider store={store}>
139 </ProtonStoreProvider>
142 expect(spy).toHaveBeenCalledTimes(1);
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>;
156 const { getByTestId } = render(
157 <ProtonStoreProvider store={store}>
159 </ProtonStoreProvider>
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);
171 <ProtonStoreProvider store={store}>
173 </ProtonStoreProvider>
176 expect(spy).toHaveBeenCalledTimes(1);
179 store.dispatch(slice.actions.reset());
183 <ProtonStoreProvider store={store}>
185 </ProtonStoreProvider>
188 expect(spy).toHaveBeenCalledTimes(1);
189 expect(div).toHaveTextContent('undefined, true');
190 await waitFor(() => expect(div).toHaveTextContent('42, false'));
192 expect(spy).toHaveBeenCalledTimes(2);
195 <ProtonStoreProvider store={store}>
197 </ProtonStoreProvider>
200 expect(spy).toHaveBeenCalledTimes(2);