@kirill.konshin/react 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ctirc +19 -10
- package/.turbo/turbo-build.log +13 -32
- package/CHANGELOG.md +14 -0
- package/README.md +56 -0
- package/dist/form/form.d.ts +10 -10
- package/dist/form/form.d.ts.map +1 -1
- package/dist/form/form.js +6 -6
- package/dist/form/form.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/keyboard.d.ts +4 -1
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +3 -3
- package/dist/keyboard.js.map +1 -1
- package/dist/useFetch.d.ts +28 -1
- package/dist/useFetch.d.ts.map +1 -1
- package/dist/useFetch.js +51 -5
- package/dist/useFetch.js.map +1 -1
- package/dist/useFetch.test.d.ts +2 -0
- package/dist/useFetch.test.d.ts.map +1 -0
- package/package.json +17 -5
- package/src/form/form.tsx +22 -22
- package/src/index.ts +1 -1
- package/src/keyboard.tsx +18 -3
- package/src/useFetch.test.tsx +424 -0
- package/src/useFetch.ts +91 -11
package/src/form/form.tsx
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
import { Context, createContext, useContext, useMemo, JSX, FC, memo } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
|
|
5
5
|
const nonEmpty = 'This field cannot be empty';
|
|
6
6
|
|
|
7
|
-
export const stringRequired = (): z.ZodString => z.string({
|
|
7
|
+
export const stringRequired = (): z.ZodString => z.string({ error: nonEmpty }).min(1, { error: nonEmpty });
|
|
8
8
|
export const maxLength = (schema: z.ZodString): number => schema.maxLength || 0;
|
|
9
9
|
export const minLength = (schema: z.ZodString): number => schema.minLength || 0;
|
|
10
10
|
export const isRequired = (schema: z.ZodString): boolean => minLength(schema) > 0;
|
|
11
11
|
|
|
12
|
-
export type ZodObject = z.ZodObject<any> | z.
|
|
13
|
-
export type MaybeTypeOf<S extends ZodObject> = Partial<
|
|
14
|
-
export type SafeTypeOf<S extends ZodObject> =
|
|
15
|
-
export type Errors<S extends ZodObject> =
|
|
12
|
+
export type ZodObject = z.ZodObject<any> | z.ZodPipe<any, any>; // z.ZodType<any, any, any>
|
|
13
|
+
export type MaybeTypeOf<S extends ZodObject> = Partial<z.output<S>>;
|
|
14
|
+
export type SafeTypeOf<S extends ZodObject> = z.core.util.SafeParseResult<z.output<S>>['data'];
|
|
15
|
+
export type Errors<S extends ZodObject> = z.core.$ZodFlattenedError<z.output<S>>['fieldErrors'];
|
|
16
16
|
export type Validation<S extends ZodObject> =
|
|
17
17
|
| {
|
|
18
18
|
success: true; // this is true only if form was validated successfully
|
|
@@ -40,13 +40,13 @@ export const Form: FC<FormProps<any>> = memo(function Form({ schema, children })
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
const getShape = <S extends ZodObject>(schema: S) =>
|
|
43
|
-
(schema as z.ZodObject<any>).shape || (schema as z.
|
|
43
|
+
(schema as z.ZodObject<any>).shape || (schema as z.ZodPipe<any, any>).in.shape;
|
|
44
44
|
|
|
45
45
|
export function create<S extends ZodObject>(
|
|
46
46
|
schema: S,
|
|
47
47
|
): {
|
|
48
48
|
register: (
|
|
49
|
-
name: keyof
|
|
49
|
+
name: keyof z.output<S>,
|
|
50
50
|
data?: MaybeTypeOf<S>,
|
|
51
51
|
errors?: Errors<S>,
|
|
52
52
|
mui?: boolean,
|
|
@@ -54,12 +54,12 @@ export function create<S extends ZodObject>(
|
|
|
54
54
|
label?: any;
|
|
55
55
|
helperText?: string;
|
|
56
56
|
error?: boolean;
|
|
57
|
-
name: keyof z.
|
|
58
|
-
id: keyof z.
|
|
57
|
+
name: keyof z.output<S>;
|
|
58
|
+
id: keyof z.output<S>;
|
|
59
59
|
required: boolean;
|
|
60
60
|
maxLength: number;
|
|
61
61
|
type: string;
|
|
62
|
-
defaultValue?: Partial<z.
|
|
62
|
+
defaultValue?: Partial<z.output<S>>[keyof z.output<S>];
|
|
63
63
|
};
|
|
64
64
|
validate: (formData: FormData) => Validation<S>;
|
|
65
65
|
validationError: (data: MaybeTypeOf<S>, errors: Errors<S>) => Validation<S>;
|
|
@@ -69,7 +69,7 @@ export function create<S extends ZodObject>(
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function register(
|
|
72
|
-
name: keyof
|
|
72
|
+
name: keyof z.output<S>,
|
|
73
73
|
data?: MaybeTypeOf<S>,
|
|
74
74
|
errors?: Errors<S>,
|
|
75
75
|
mui: boolean = false,
|
|
@@ -77,12 +77,12 @@ export function create<S extends ZodObject>(
|
|
|
77
77
|
label?: any;
|
|
78
78
|
helperText?: string;
|
|
79
79
|
error?: boolean;
|
|
80
|
-
name: keyof z.
|
|
81
|
-
id: keyof z.
|
|
80
|
+
name: keyof z.output<S>;
|
|
81
|
+
id: keyof z.output<S>;
|
|
82
82
|
required: boolean;
|
|
83
83
|
maxLength: number;
|
|
84
84
|
type: string;
|
|
85
|
-
defaultValue?: Partial<z.
|
|
85
|
+
defaultValue?: Partial<z.output<S>>[keyof z.output<S>];
|
|
86
86
|
} {
|
|
87
87
|
const field = getShape(schema)[name];
|
|
88
88
|
return {
|
|
@@ -111,18 +111,18 @@ export function create<S extends ZodObject>(
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
function validate(formData: FormData): Validation<S> {
|
|
114
|
-
const rawData = Object.fromEntries(formData) as
|
|
115
|
-
const
|
|
114
|
+
const rawData = Object.fromEntries(formData) as z.output<S>;
|
|
115
|
+
const result = schema.safeParse(rawData);
|
|
116
116
|
|
|
117
117
|
// console.log('Validate result', { error, data, rawData });
|
|
118
118
|
|
|
119
|
-
if (
|
|
119
|
+
if (!result.success) {
|
|
120
120
|
// data is undefined if there are errors
|
|
121
121
|
// Next.js will butcher error object, so we provide something more primitive
|
|
122
|
-
return validationError(rawData,
|
|
122
|
+
return validationError(rawData, z.flattenError(result.error).fieldErrors as any);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
return { success: true, data };
|
|
125
|
+
return { success: true, data: result.data };
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
return { register, validate, validationError };
|
|
@@ -130,7 +130,7 @@ export function create<S extends ZodObject>(
|
|
|
130
130
|
|
|
131
131
|
interface FieldProps<S extends ZodObject> {
|
|
132
132
|
children?: any;
|
|
133
|
-
name: keyof
|
|
133
|
+
name: keyof z.output<S>;
|
|
134
134
|
errors?: Validation<S>['errors'];
|
|
135
135
|
hint?: string;
|
|
136
136
|
className?: string;
|
|
@@ -159,7 +159,7 @@ export const Field: FC<FieldProps<any>> = memo(function Field({
|
|
|
159
159
|
)}
|
|
160
160
|
{children}
|
|
161
161
|
{hint && <Hint>{hint}</Hint>}
|
|
162
|
-
{errors?.[name]?.map((e: string) => (
|
|
162
|
+
{errors?.[name as any]?.map((e: string) => (
|
|
163
163
|
<Hint error key={e}>
|
|
164
164
|
{e}
|
|
165
165
|
</Hint>
|
package/src/index.ts
CHANGED
package/src/keyboard.tsx
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
createContext,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
Dispatch,
|
|
9
|
+
SetStateAction,
|
|
10
|
+
useContext,
|
|
11
|
+
FC,
|
|
12
|
+
memo,
|
|
13
|
+
Context,
|
|
14
|
+
} from 'react';
|
|
4
15
|
|
|
5
16
|
const isCtrlOrMeta = (e: KeyboardEvent) => e.metaKey || e.ctrlKey;
|
|
6
17
|
|
|
@@ -18,13 +29,17 @@ export const HotkeysContext: Context<HotkeyContextType> = createContext<{
|
|
|
18
29
|
setEnabled: Dispatch<SetStateAction<boolean>>;
|
|
19
30
|
}>(null as never);
|
|
20
31
|
|
|
21
|
-
export
|
|
32
|
+
export type HotkeysProviderProps = {
|
|
33
|
+
children: any;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const HotkeysProvider: FC<HotkeysProviderProps> = memo(function HotkeysProvider({ children }) {
|
|
22
37
|
const [enabled, setEnabled] = useState(true);
|
|
23
38
|
|
|
24
39
|
const control = useMemo(() => ({ enabled, setEnabled }), [enabled, setEnabled]);
|
|
25
40
|
|
|
26
41
|
return <HotkeysContext.Provider value={control}>{children}</HotkeysContext.Provider>;
|
|
27
|
-
};
|
|
42
|
+
});
|
|
28
43
|
|
|
29
44
|
export const useHotkeys = (hotkeys: Hotkeys): void => {
|
|
30
45
|
const { enabled } = useContext(HotkeysContext);
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { expect, describe, test, vi } from 'vitest';
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
3
|
+
import { useFetch } from './useFetch';
|
|
4
|
+
import { act } from 'react';
|
|
5
|
+
|
|
6
|
+
describe('useFetch', () => {
|
|
7
|
+
test('initializes with default value', () => {
|
|
8
|
+
const mockFn = vi.fn(async () => 'result');
|
|
9
|
+
const { result } = renderHook(() => useFetch(mockFn, 'default'));
|
|
10
|
+
|
|
11
|
+
const [data, actionFn, isPending, error] = result.current;
|
|
12
|
+
|
|
13
|
+
expect(data).toBe('default');
|
|
14
|
+
expect(typeof actionFn).toBe('function');
|
|
15
|
+
expect(isPending).toBe(false);
|
|
16
|
+
expect(error).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('executes function and updates data on success', async () => {
|
|
20
|
+
const mockFn = vi.fn(async () => 'success');
|
|
21
|
+
const { result } = renderHook(() => useFetch(mockFn, 'default'));
|
|
22
|
+
|
|
23
|
+
await act(async () => {
|
|
24
|
+
result.current[1]();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await waitFor(() => {
|
|
28
|
+
expect(result.current[2]).toBe(false); // isPending becomes false
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(result.current[0]).toBe('success');
|
|
32
|
+
expect(result.current[3]).toBeUndefined();
|
|
33
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('handles function arguments', async () => {
|
|
37
|
+
const mockFn = vi.fn(async (a: number, b: string) => `${a}-${b}`);
|
|
38
|
+
const { result } = renderHook(() => useFetch(mockFn, 'default'));
|
|
39
|
+
|
|
40
|
+
await act(async () => {
|
|
41
|
+
result.current[1](42, 'test');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await waitFor(() => {
|
|
45
|
+
expect(result.current[0]).toBe('42-test');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(mockFn).toHaveBeenCalledWith(42, 'test');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('sets isPending to true during execution', async () => {
|
|
52
|
+
let resolvePromise: (value: string) => void;
|
|
53
|
+
const promise = new Promise<string>((resolve) => {
|
|
54
|
+
resolvePromise = resolve;
|
|
55
|
+
});
|
|
56
|
+
const mockFn = vi.fn(async () => promise);
|
|
57
|
+
const { result } = renderHook(() => useFetch(mockFn, 'default'));
|
|
58
|
+
|
|
59
|
+
act(() => {
|
|
60
|
+
result.current[1]();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Should be pending immediately
|
|
64
|
+
expect(result.current[2]).toBe(true);
|
|
65
|
+
|
|
66
|
+
await act(async () => {
|
|
67
|
+
resolvePromise!('resolved');
|
|
68
|
+
await promise;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(result.current[2]).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result.current[0]).toBe('resolved');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('captures and stores errors', async () => {
|
|
79
|
+
const error = new Error('Test error');
|
|
80
|
+
const mockFn = vi.fn(async () => {
|
|
81
|
+
throw error;
|
|
82
|
+
});
|
|
83
|
+
const { result } = renderHook(() => useFetch(mockFn, 'default'));
|
|
84
|
+
|
|
85
|
+
await act(async () => {
|
|
86
|
+
result.current[1]();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(result.current[3]).toBe(error);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result.current[0]).toBe('default'); // data remains unchanged
|
|
94
|
+
expect(result.current[2]).toBe(false); // isPending becomes false
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('does not update data if component unmounts before resolution', async () => {
|
|
98
|
+
let resolvePromise: (value: string) => void;
|
|
99
|
+
const promise = new Promise<string>((resolve) => {
|
|
100
|
+
resolvePromise = resolve;
|
|
101
|
+
});
|
|
102
|
+
const mockFn = vi.fn(async () => promise);
|
|
103
|
+
const { result, unmount } = renderHook(() => useFetch(mockFn, 'default'));
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
result.current[1]();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
unmount();
|
|
110
|
+
|
|
111
|
+
await act(async () => {
|
|
112
|
+
resolvePromise!('resolved');
|
|
113
|
+
await promise;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// No assertions needed - just verifying no errors on unmount
|
|
117
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('returns the promise from actionFn', async () => {
|
|
121
|
+
const mockFn = vi.fn(async () => 'result');
|
|
122
|
+
const { result } = renderHook(() => useFetch(mockFn, 'default'));
|
|
123
|
+
|
|
124
|
+
let returnedPromise: Promise<string>;
|
|
125
|
+
await act(async () => {
|
|
126
|
+
returnedPromise = result.current[1]();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await expect(returnedPromise!).resolves.toBe('result');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('handles multiple sequential calls', async () => {
|
|
133
|
+
const mockFn = vi.fn(async (n: number) => `result-${n}`);
|
|
134
|
+
const { result } = renderHook(() => useFetch(mockFn, 'default'));
|
|
135
|
+
|
|
136
|
+
await act(async () => {
|
|
137
|
+
result.current[1](1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(result.current[0]).toBe('result-1');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await act(async () => {
|
|
145
|
+
result.current[1](2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(result.current[0]).toBe('result-2');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('preserves actionFn reference when fn dependency does not change', () => {
|
|
156
|
+
const mockFn = vi.fn(async () => 'result');
|
|
157
|
+
const { result, rerender } = renderHook(() => useFetch(mockFn, 'default'));
|
|
158
|
+
|
|
159
|
+
const firstActionFn = result.current[1];
|
|
160
|
+
|
|
161
|
+
rerender();
|
|
162
|
+
|
|
163
|
+
const secondActionFn = result.current[1];
|
|
164
|
+
|
|
165
|
+
expect(firstActionFn).toBe(secondActionFn);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('updates actionFn when fn dependency changes', async () => {
|
|
169
|
+
const mockFn1 = vi.fn(async () => 'result1');
|
|
170
|
+
const mockFn2 = vi.fn(async () => 'result2');
|
|
171
|
+
|
|
172
|
+
const { result, rerender } = renderHook(({ fn }) => useFetch(fn, 'default'), {
|
|
173
|
+
initialProps: { fn: mockFn1 },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const firstActionFn = result.current[1];
|
|
177
|
+
|
|
178
|
+
await act(async () => {
|
|
179
|
+
result.current[1]();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
expect(result.current[0]).toBe('result1');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Change the function
|
|
187
|
+
rerender({ fn: mockFn2 });
|
|
188
|
+
|
|
189
|
+
const secondActionFn = result.current[1];
|
|
190
|
+
|
|
191
|
+
expect(firstActionFn).not.toBe(secondActionFn);
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
result.current[1]();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await waitFor(() => {
|
|
198
|
+
expect(result.current[0]).toBe('result2');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(mockFn1).toHaveBeenCalledTimes(1);
|
|
202
|
+
expect(mockFn2).toHaveBeenCalledTimes(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('does not trigger execution on rerender', () => {
|
|
206
|
+
const mockFn = vi.fn(async () => 'result');
|
|
207
|
+
const { rerender } = renderHook(() => useFetch(mockFn, 'default'));
|
|
208
|
+
|
|
209
|
+
rerender();
|
|
210
|
+
rerender();
|
|
211
|
+
rerender();
|
|
212
|
+
|
|
213
|
+
expect(mockFn).not.toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('handles concurrent calls correctly', async () => {
|
|
217
|
+
let callCount = 0;
|
|
218
|
+
const mockFn = vi.fn(async () => {
|
|
219
|
+
callCount++;
|
|
220
|
+
const currentCall = callCount;
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
222
|
+
return `result-${currentCall}`;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const { result } = renderHook(() => useFetch(mockFn, 'default'));
|
|
226
|
+
|
|
227
|
+
// Fire multiple calls at once
|
|
228
|
+
await act(async () => {
|
|
229
|
+
result.current[1]();
|
|
230
|
+
result.current[1]();
|
|
231
|
+
result.current[1]();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
expect(result.current[2]).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Last call should win
|
|
239
|
+
expect(mockFn).toHaveBeenCalledTimes(3);
|
|
240
|
+
expect(['result-1', 'result-2', 'result-3']).toContain(result.current[0]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('maintains separate state for different hook instances', async () => {
|
|
244
|
+
const mockFn1 = vi.fn(async () => 'result1');
|
|
245
|
+
const mockFn2 = vi.fn(async () => 'result2');
|
|
246
|
+
|
|
247
|
+
const { result: result1 } = renderHook(() => useFetch(mockFn1, 'default1'));
|
|
248
|
+
const { result: result2 } = renderHook(() => useFetch(mockFn2, 'default2'));
|
|
249
|
+
|
|
250
|
+
expect(result1.current[0]).toBe('default1');
|
|
251
|
+
expect(result2.current[0]).toBe('default2');
|
|
252
|
+
|
|
253
|
+
await act(async () => {
|
|
254
|
+
result1.current[1]();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await waitFor(() => {
|
|
258
|
+
expect(result1.current[0]).toBe('result1');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(result2.current[0]).toBe('default2'); // unchanged
|
|
262
|
+
|
|
263
|
+
await act(async () => {
|
|
264
|
+
result2.current[1]();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
expect(result2.current[0]).toBe('result2');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(result1.current[0]).toBe('result1'); // still unchanged
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('handles closure over external state', async () => {
|
|
275
|
+
let externalValue = 10;
|
|
276
|
+
|
|
277
|
+
const { result, rerender } = renderHook(() => {
|
|
278
|
+
const mockFn = async () => `value-${externalValue}`;
|
|
279
|
+
return useFetch(mockFn, 'default');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await act(async () => {
|
|
283
|
+
result.current[1]();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await waitFor(() => {
|
|
287
|
+
expect(result.current[0]).toBe('value-10');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Change external value and rerender
|
|
291
|
+
externalValue = 20;
|
|
292
|
+
rerender();
|
|
293
|
+
|
|
294
|
+
await act(async () => {
|
|
295
|
+
result.current[1]();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
expect(result.current[0]).toBe('value-20');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('clears error on successful subsequent call', async () => {
|
|
304
|
+
const error = new Error('Test error');
|
|
305
|
+
let shouldError = true;
|
|
306
|
+
|
|
307
|
+
const mockFn = vi.fn(async () => {
|
|
308
|
+
if (shouldError) {
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
return 'success';
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const { result } = renderHook(() => useFetch(mockFn, 'default'));
|
|
315
|
+
|
|
316
|
+
// First call errors
|
|
317
|
+
await act(async () => {
|
|
318
|
+
result.current[1]();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await waitFor(() => {
|
|
322
|
+
expect(result.current[3]).toBe(error);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Second call succeeds
|
|
326
|
+
shouldError = false;
|
|
327
|
+
await act(async () => {
|
|
328
|
+
result.current[1]();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await waitFor(() => {
|
|
332
|
+
expect(result.current[0]).toBe('success');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Error is cleared automatically
|
|
336
|
+
expect(result.current[3]).toBe(undefined);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('handles callback that returns a function with old value', async () => {
|
|
340
|
+
const mockFn = vi.fn((increment: number) => (oldValue: number) => oldValue + increment);
|
|
341
|
+
const { result } = renderHook(() => useFetch(mockFn, 0));
|
|
342
|
+
|
|
343
|
+
// First call - should call the function with oldValue (0) and get 0 + 5 = 5
|
|
344
|
+
await act(async () => {
|
|
345
|
+
result.current[1](5);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await waitFor(() => {
|
|
349
|
+
expect(result.current[0]).toBe(5);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Second call - should call the function with oldValue (5) and get 5 + 10 = 15
|
|
353
|
+
await act(async () => {
|
|
354
|
+
result.current[1](10);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await waitFor(() => {
|
|
358
|
+
expect(result.current[0]).toBe(15);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('handles multiple calls with old value pattern', async () => {
|
|
365
|
+
const mockFn = vi.fn(
|
|
366
|
+
(operation: string, value: number) => (oldValue: string) => `${oldValue}-${operation}${value}`,
|
|
367
|
+
);
|
|
368
|
+
const { result } = renderHook(() => useFetch(mockFn, 'initial'));
|
|
369
|
+
|
|
370
|
+
// First call
|
|
371
|
+
await act(async () => {
|
|
372
|
+
result.current[1]('add', 1);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
await waitFor(() => {
|
|
376
|
+
expect(result.current[0]).toBe('initial-add1');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Second call - should use the updated value
|
|
380
|
+
await act(async () => {
|
|
381
|
+
result.current[1]('multiply', 2);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await waitFor(() => {
|
|
385
|
+
expect(result.current[0]).toBe('initial-add1-multiply2');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('handles counter pattern with old value', async () => {
|
|
392
|
+
const increment = vi.fn(() => (prev: number) => prev + 1);
|
|
393
|
+
const { result } = renderHook(() => useFetch(increment, 0));
|
|
394
|
+
|
|
395
|
+
// First increment: 0 + 1 = 1
|
|
396
|
+
await act(async () => {
|
|
397
|
+
result.current[1]();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await waitFor(() => {
|
|
401
|
+
expect(result.current[0]).toBe(1);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Second increment: 1 + 1 = 2
|
|
405
|
+
await act(async () => {
|
|
406
|
+
result.current[1]();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await waitFor(() => {
|
|
410
|
+
expect(result.current[0]).toBe(2);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Third increment: 2 + 1 = 3
|
|
414
|
+
await act(async () => {
|
|
415
|
+
result.current[1]();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await waitFor(() => {
|
|
419
|
+
expect(result.current[0]).toBe(3);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
expect(increment).toHaveBeenCalledTimes(3);
|
|
423
|
+
});
|
|
424
|
+
});
|