1 import type { PropsWithChildren, ReactNode } from 'react';
2 import { Provider as ReduxProvider } from 'react-redux';
4 import { type UnknownAction, configureStore, createAction } from '@reduxjs/toolkit';
5 import { act, renderHook } from '@testing-library/react-hooks';
7 import { withRequest } from '@proton/pass/store/request/enhancers';
8 import { requestMiddleware } from '@proton/pass/store/request/middleware';
9 import request from '@proton/pass/store/request/reducer';
11 import { useActionRequest } from './useActionRequest';
13 const requestId = 'test::id';
15 const start = createAction('test::start', () => withRequest({ status: 'start', id: requestId })({ payload: {} }));
16 const success = createAction('test::success', () => withRequest({ status: 'success', id: requestId })({ payload: {} }));
17 const failure = createAction('test::failure', () => withRequest({ status: 'failure', id: requestId })({ payload: {} }));
19 const successCache = createAction('test::success::cache', () =>
20 withRequest({ status: 'success', id: requestId, maxAge: 1 })({ payload: {} })
23 const buildHook = (useInitialRequestId: boolean = true, initialActions: UnknownAction[] = []) => {
24 const store = configureStore({ reducer: { request }, middleware: (mw) => mw().concat(requestMiddleware) });
25 initialActions.forEach((action) => store.dispatch(action));
27 const onStart = jest.fn();
28 const onSuccess = jest.fn();
29 const onFailure = jest.fn();
36 hook: renderHook<PropsWithChildren, ReturnType<typeof useActionRequest>>(
38 useActionRequest(start, {
39 initialRequestId: useInitialRequestId ? requestId : undefined,
45 wrapper: (({ children }: { children: ReactNode }) => (
46 <ReduxProvider store={store}>{children}</ReduxProvider>
53 describe('useActionRequest', () => {
54 it('Handles request sequence', async () => {
55 const { hook, onStart, onSuccess, onFailure, store } = buildHook();
57 expect(hook.result.current.loading).toBe(false);
58 expect(hook.result.current.progress).toEqual(0);
59 expect(onStart).not.toHaveBeenCalled();
60 expect(onSuccess).not.toHaveBeenCalled();
61 expect(onFailure).not.toHaveBeenCalled();
64 hook.result.current.dispatch();
67 expect(hook.result.current.loading).toBe(true);
68 expect(hook.result.current.progress).toEqual(0);
69 expect(onStart).toHaveBeenCalledTimes(1);
70 expect(onSuccess).not.toHaveBeenCalled();
71 expect(onFailure).not.toHaveBeenCalled();
74 store.dispatch(success());
77 expect(hook.result.current.loading).toBe(false);
78 expect(hook.result.current.progress).toBe(100);
79 expect(onStart).toHaveBeenCalledTimes(1);
80 expect(onSuccess).toHaveBeenCalledTimes(1);
81 expect(onFailure).not.toHaveBeenCalled();
84 hook.result.current.dispatch();
87 expect(hook.result.current.loading).toBe(true);
88 expect(hook.result.current.progress).toEqual(0);
89 expect(onStart).toHaveBeenCalledTimes(2);
90 expect(onSuccess).toHaveBeenCalledTimes(1);
91 expect(onFailure).not.toHaveBeenCalled();
94 store.dispatch(failure());
97 expect(hook.result.current.loading).toBe(false);
98 expect(hook.result.current.progress).toBe(100);
99 expect(onStart).toHaveBeenCalledTimes(2);
100 expect(onSuccess).toHaveBeenCalledTimes(1);
101 expect(onFailure).toHaveBeenCalledTimes(1);
104 test('Revalidate should re-trigger sequence', () => {
105 const { hook, onStart, onSuccess, onFailure } = buildHook(true, [successCache()]);
108 hook.result.current.revalidate();
111 expect(hook.result.current.loading).toBe(true);
112 expect(hook.result.current.progress).toEqual(0);
113 expect(onStart).toHaveBeenCalledTimes(1);
114 expect(onSuccess).toHaveBeenCalledTimes(1);
115 expect(onFailure).not.toHaveBeenCalled();
118 test('dispatching should noop if request success with maxAge', () => {
119 const { hook, onStart, onSuccess, onFailure } = buildHook(false, [successCache()]);
122 hook.result.current.dispatch();
125 expect(hook.result.current.loading).toBe(false);
126 expect(hook.result.current.progress).toEqual(100);
127 expect(onStart).not.toHaveBeenCalled();
128 expect(onSuccess).toHaveBeenCalledTimes(1);
129 expect(onFailure).not.toHaveBeenCalled();
132 test('dispatching the same action should only trigger effect once', () => {
133 const { hook, onStart, onSuccess, onFailure } = buildHook(false, []);
136 hook.result.current.dispatch();
137 hook.result.current.dispatch();
138 hook.result.current.dispatch();
139 hook.result.current.dispatch();
142 expect(hook.result.current.loading).toBe(true);
143 expect(hook.result.current.progress).toEqual(0);
144 expect(onStart).toHaveBeenCalledTimes(1);
145 expect(onSuccess).not.toHaveBeenCalled();
146 expect(onFailure).not.toHaveBeenCalled();
149 test('Setting `initialRequestId` should trigger `onStart`', () => {
150 const { hook, onStart, onSuccess, onFailure } = buildHook(true, [start()]);
152 expect(hook.result.current.loading).toBe(true);
153 expect(hook.result.current.progress).toEqual(0);
154 expect(onStart).toHaveBeenCalledTimes(1);
155 expect(onSuccess).not.toHaveBeenCalled();
156 expect(onFailure).not.toHaveBeenCalled();
159 test('Setting `initialRequestId` should trigger `onSuccess`', () => {
160 const { hook, onStart, onSuccess, onFailure } = buildHook(true, [success()]);
162 expect(hook.result.current.loading).toBe(false);
163 expect(hook.result.current.progress).toEqual(100);
164 expect(onStart).not.toHaveBeenCalled();
165 expect(onSuccess).toHaveBeenCalledTimes(1);
166 expect(onFailure).not.toHaveBeenCalled();
169 test('Setting `initialRequestId` should trigger `onFailure`', () => {
170 const { hook, onStart, onSuccess, onFailure } = buildHook(true, [failure()]);
172 expect(hook.result.current.loading).toBe(false);
173 expect(hook.result.current.progress).toEqual(100);
174 expect(onStart).not.toHaveBeenCalled();
175 expect(onSuccess).not.toHaveBeenCalled();
176 expect(onFailure).toHaveBeenCalledTimes(1);