@blastlabs/utils 1.11.0 → 1.12.0
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/dist/components/dev/ApiLogger.d.ts +1 -1
- package/dist/components/dev/ApiLogger.js +2 -2
- package/dist/components/dev/DevPanel.d.ts +1 -1
- package/dist/components/dev/DevPanel.js +2 -2
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts +1 -1
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts.map +1 -1
- package/dist/components/dev/FormDevTools/FormDevTools.js +11 -11
- package/dist/components/dev/FormDevTools/index.d.ts +1 -1
- package/dist/components/dev/FormDevTools/index.d.ts.map +1 -1
- package/dist/components/dev/{IdSelector.d.ts → IdSelector/IdSelector.d.ts} +3 -4
- package/dist/components/dev/IdSelector/IdSelector.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/IdSelector.js +60 -0
- package/dist/components/dev/IdSelector/IdSelector.test.d.ts +2 -0
- package/dist/components/dev/IdSelector/IdSelector.test.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/IdSelector.test.js +203 -0
- package/dist/components/dev/IdSelector/LoginCard.d.ts +17 -0
- package/dist/components/dev/IdSelector/LoginCard.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/LoginCard.js +16 -0
- package/dist/components/dev/IdSelector/index.d.ts +3 -0
- package/dist/components/dev/IdSelector/index.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/index.js +1 -0
- package/dist/components/dev/IdSelector/styles.d.ts +16 -0
- package/dist/components/dev/IdSelector/styles.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/styles.js +66 -0
- package/dist/components/dev/WindowSizeDisplay.d.ts +1 -1
- package/dist/components/dev/WindowSizeDisplay.js +2 -2
- package/dist/components/dev/ZIndexDebugger.d.ts +1 -1
- package/dist/components/dev/ZIndexDebugger.js +1 -1
- package/dist/components/dev/index.d.ts +2 -1
- package/dist/components/dev/index.d.ts.map +1 -1
- package/dist/hooks/event/index.d.ts +6 -0
- package/dist/hooks/event/index.d.ts.map +1 -0
- package/dist/hooks/event/index.js +5 -0
- package/dist/hooks/event/useClickOutside.d.ts.map +1 -0
- package/dist/hooks/event/useEventListener.d.ts.map +1 -0
- package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts +2 -0
- package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts.map +1 -0
- package/dist/hooks/form/__tests__/useCRUDForm.test.js +487 -0
- package/dist/hooks/form/index.d.ts +5 -0
- package/dist/hooks/form/index.d.ts.map +1 -0
- package/dist/hooks/form/index.js +4 -0
- package/dist/hooks/form/useCRUDForm.d.ts +232 -0
- package/dist/hooks/form/useCRUDForm.d.ts.map +1 -0
- package/dist/hooks/form/useCRUDForm.js +287 -0
- package/dist/hooks/index.d.ts +9 -14
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +16 -19
- package/dist/hooks/performance/index.d.ts +7 -0
- package/dist/hooks/performance/index.d.ts.map +1 -0
- package/dist/hooks/performance/index.js +6 -0
- package/dist/hooks/performance/useDebounce.d.ts.map +1 -0
- package/dist/hooks/performance/useIntersectionObserver.d.ts.map +1 -0
- package/dist/hooks/performance/useThrottle.d.ts.map +1 -0
- package/dist/hooks/state/index.d.ts +6 -0
- package/dist/hooks/state/index.d.ts.map +1 -0
- package/dist/hooks/state/index.js +5 -0
- package/dist/hooks/state/usePrevious.d.ts.map +1 -0
- package/dist/hooks/state/useToggle.d.ts.map +1 -0
- package/dist/hooks/storage/index.d.ts +7 -0
- package/dist/hooks/storage/index.d.ts.map +1 -0
- package/dist/hooks/storage/index.js +6 -0
- package/dist/hooks/storage/useCopyToClipboard.d.ts.map +1 -0
- package/dist/hooks/storage/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/storage/useSessionStorage.d.ts.map +1 -0
- package/dist/hooks/time/__tests__/useCountdown.test.d.ts +2 -0
- package/dist/hooks/time/__tests__/useCountdown.test.d.ts.map +1 -0
- package/dist/hooks/time/__tests__/useCountdown.test.js +150 -0
- package/dist/hooks/time/__tests__/useInterval.test.d.ts +2 -0
- package/dist/hooks/time/__tests__/useInterval.test.d.ts.map +1 -0
- package/dist/hooks/time/__tests__/useInterval.test.js +39 -0
- package/dist/hooks/time/__tests__/useStopwatch.test.d.ts +2 -0
- package/dist/hooks/time/__tests__/useStopwatch.test.d.ts.map +1 -0
- package/dist/hooks/time/__tests__/useStopwatch.test.js +149 -0
- package/dist/hooks/time/index.d.ts +7 -0
- package/dist/hooks/time/index.d.ts.map +1 -0
- package/dist/hooks/time/index.js +6 -0
- package/dist/hooks/time/useCountdown.d.ts +116 -0
- package/dist/hooks/time/useCountdown.d.ts.map +1 -0
- package/dist/hooks/time/useCountdown.js +152 -0
- package/dist/hooks/time/useInterval.d.ts +40 -0
- package/dist/hooks/time/useInterval.d.ts.map +1 -0
- package/dist/hooks/time/useInterval.js +61 -0
- package/dist/hooks/time/useStopwatch.d.ts +142 -0
- package/dist/hooks/time/useStopwatch.d.ts.map +1 -0
- package/dist/hooks/time/useStopwatch.js +179 -0
- package/dist/hooks/ui/index.d.ts +6 -0
- package/dist/hooks/ui/index.d.ts.map +1 -0
- package/dist/hooks/ui/index.js +5 -0
- package/dist/hooks/ui/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/ui/useWindowSize.d.ts.map +1 -0
- package/package.json +14 -4
- package/dist/components/dev/IdSelector.d.ts.map +0 -1
- package/dist/components/dev/IdSelector.js +0 -129
- package/dist/hooks/useClickOutside.d.ts.map +0 -1
- package/dist/hooks/useCopyToClipboard.d.ts.map +0 -1
- package/dist/hooks/useDebounce.d.ts.map +0 -1
- package/dist/hooks/useEventListener.d.ts.map +0 -1
- package/dist/hooks/useIntersectionObserver.d.ts.map +0 -1
- package/dist/hooks/useLocalStorage.d.ts.map +0 -1
- package/dist/hooks/useMediaQuery.d.ts.map +0 -1
- package/dist/hooks/usePrevious.d.ts.map +0 -1
- package/dist/hooks/useSessionStorage.d.ts.map +0 -1
- package/dist/hooks/useThrottle.d.ts.map +0 -1
- package/dist/hooks/useToggle.d.ts.map +0 -1
- package/dist/hooks/useWindowSize.d.ts.map +0 -1
- /package/dist/hooks/{useClickOutside.d.ts → event/useClickOutside.d.ts} +0 -0
- /package/dist/hooks/{useClickOutside.js → event/useClickOutside.js} +0 -0
- /package/dist/hooks/{useEventListener.d.ts → event/useEventListener.d.ts} +0 -0
- /package/dist/hooks/{useEventListener.js → event/useEventListener.js} +0 -0
- /package/dist/hooks/{useDebounce.d.ts → performance/useDebounce.d.ts} +0 -0
- /package/dist/hooks/{useDebounce.js → performance/useDebounce.js} +0 -0
- /package/dist/hooks/{useIntersectionObserver.d.ts → performance/useIntersectionObserver.d.ts} +0 -0
- /package/dist/hooks/{useIntersectionObserver.js → performance/useIntersectionObserver.js} +0 -0
- /package/dist/hooks/{useThrottle.d.ts → performance/useThrottle.d.ts} +0 -0
- /package/dist/hooks/{useThrottle.js → performance/useThrottle.js} +0 -0
- /package/dist/hooks/{usePrevious.d.ts → state/usePrevious.d.ts} +0 -0
- /package/dist/hooks/{usePrevious.js → state/usePrevious.js} +0 -0
- /package/dist/hooks/{useToggle.d.ts → state/useToggle.d.ts} +0 -0
- /package/dist/hooks/{useToggle.js → state/useToggle.js} +0 -0
- /package/dist/hooks/{useCopyToClipboard.d.ts → storage/useCopyToClipboard.d.ts} +0 -0
- /package/dist/hooks/{useCopyToClipboard.js → storage/useCopyToClipboard.js} +0 -0
- /package/dist/hooks/{useLocalStorage.d.ts → storage/useLocalStorage.d.ts} +0 -0
- /package/dist/hooks/{useLocalStorage.js → storage/useLocalStorage.js} +0 -0
- /package/dist/hooks/{useSessionStorage.d.ts → storage/useSessionStorage.d.ts} +0 -0
- /package/dist/hooks/{useSessionStorage.js → storage/useSessionStorage.js} +0 -0
- /package/dist/hooks/{useMediaQuery.d.ts → ui/useMediaQuery.d.ts} +0 -0
- /package/dist/hooks/{useMediaQuery.js → ui/useMediaQuery.js} +0 -0
- /package/dist/hooks/{useWindowSize.d.ts → ui/useWindowSize.d.ts} +0 -0
- /package/dist/hooks/{useWindowSize.js → ui/useWindowSize.js} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { useWindowSize } from '../../hooks
|
|
2
|
+
import { useWindowSize } from '../../hooks';
|
|
3
3
|
const positionStyles = {
|
|
4
4
|
'top-left': { top: 16, left: 16 },
|
|
5
5
|
'top-right': { top: 16, right: 16 },
|
|
@@ -13,7 +13,7 @@ const positionStyles = {
|
|
|
13
13
|
* @example
|
|
14
14
|
* ```tsx
|
|
15
15
|
* // Vite 프로젝트
|
|
16
|
-
* import { WindowSizeDisplay } from '
|
|
16
|
+
* import { WindowSizeDisplay } from '@blastlabs/utils/components/dev';
|
|
17
17
|
*
|
|
18
18
|
* function App() {
|
|
19
19
|
* return (
|
|
@@ -6,7 +6,7 @@ import React, { useState, useEffect } from 'react';
|
|
|
6
6
|
* @example
|
|
7
7
|
* ```tsx
|
|
8
8
|
* // Vite 프로젝트
|
|
9
|
-
* import { ZIndexDebugger } from '
|
|
9
|
+
* import { ZIndexDebugger } from '@blastlabs/utils/components/dev';
|
|
10
10
|
*
|
|
11
11
|
* function App() {
|
|
12
12
|
* return (
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* production 환경에서는 제외하는 것을 권장합니다.
|
|
6
6
|
*/
|
|
7
7
|
export { default as IdSelector } from './IdSelector';
|
|
8
|
+
export type { IdSelectorProps, LoginInfo } from './IdSelector';
|
|
8
9
|
export { default as WindowSizeDisplay } from './WindowSizeDisplay';
|
|
9
10
|
export { default as DevPanel } from './DevPanel';
|
|
10
11
|
export { default as ZIndexDebugger } from './ZIndexDebugger';
|
|
11
12
|
export { default as ApiLogger, addApiLog, clearApiLogs } from './ApiLogger';
|
|
12
13
|
export { default as FormDevTools } from './FormDevTools';
|
|
13
|
-
export type { FormDevToolsProps
|
|
14
|
+
export type { FormDevToolsProps } from './FormDevTools';
|
|
14
15
|
export type { ApiLogEntry } from './ApiLogger';
|
|
15
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/dev/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/dev/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/event/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useClickOutside.d.ts","sourceRoot":"","sources":["../../../src/hooks/event/useClickOutside.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EACjE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,KAAK,IAAI,EACjD,OAAO,GAAE,OAAc,GACtB,IAAI,CA2BN;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EACzE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,EACpB,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,KAAK,IAAI,EACjD,OAAO,GAAE,OAAc,GACtB,IAAI,CA6BN"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEventListener.d.ts","sourceRoot":"","sources":["../../../src/hooks/event/useEventListener.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,cAAc,EAC7D,SAAS,EAAE,CAAC,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,EAC3C,OAAO,CAAC,EAAE,SAAS,EACnB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;AAER,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,mBAAmB,EAAE,CAAC,SAAS,WAAW,GAAG,cAAc,EAC1G,SAAS,EAAE,CAAC,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI,EAChD,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EACrB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;AAER,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAC/D,SAAS,EAAE,CAAC,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,IAAI,EAC7C,OAAO,EAAE,QAAQ,EACjB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCRUDForm.test.d.ts","sourceRoot":"","sources":["../../../../src/hooks/form/__tests__/useCRUDForm.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
3
|
+
import { useCRUDForm } from '../useCRUDForm';
|
|
4
|
+
// Mock form object
|
|
5
|
+
const createMockForm = (defaultValues = {}) => ({
|
|
6
|
+
watch: vi.fn((name) => name ? defaultValues[name] : defaultValues),
|
|
7
|
+
getValues: vi.fn((name) => name ? defaultValues[name] : defaultValues),
|
|
8
|
+
setValue: vi.fn(),
|
|
9
|
+
reset: vi.fn(),
|
|
10
|
+
trigger: vi.fn().mockResolvedValue(true),
|
|
11
|
+
formState: {
|
|
12
|
+
errors: {},
|
|
13
|
+
dirtyFields: {},
|
|
14
|
+
touchedFields: {},
|
|
15
|
+
isValid: true,
|
|
16
|
+
isSubmitting: false,
|
|
17
|
+
submitCount: 0,
|
|
18
|
+
defaultValues,
|
|
19
|
+
isDirty: false,
|
|
20
|
+
isValidating: false,
|
|
21
|
+
isLoading: false,
|
|
22
|
+
isSubmitted: false,
|
|
23
|
+
isSubmitSuccessful: false,
|
|
24
|
+
},
|
|
25
|
+
handleSubmit: vi.fn((onValid) => async (e) => {
|
|
26
|
+
e?.preventDefault?.();
|
|
27
|
+
await onValid(defaultValues);
|
|
28
|
+
}),
|
|
29
|
+
getFieldState: vi.fn(),
|
|
30
|
+
setError: vi.fn(),
|
|
31
|
+
clearErrors: vi.fn(),
|
|
32
|
+
resetField: vi.fn(),
|
|
33
|
+
setFocus: vi.fn(),
|
|
34
|
+
unregister: vi.fn(),
|
|
35
|
+
register: vi.fn(),
|
|
36
|
+
control: {},
|
|
37
|
+
});
|
|
38
|
+
describe('useCRUDForm', () => {
|
|
39
|
+
describe('Mode Detection', () => {
|
|
40
|
+
it('should be in create mode when no id is provided', () => {
|
|
41
|
+
const form = createMockForm();
|
|
42
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
43
|
+
form,
|
|
44
|
+
}));
|
|
45
|
+
expect(result.current.mode).toBe('create');
|
|
46
|
+
expect(result.current.isCreateMode).toBe(true);
|
|
47
|
+
expect(result.current.isEditMode).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('should be in edit mode when id is provided', () => {
|
|
50
|
+
const form = createMockForm();
|
|
51
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
52
|
+
id: '123',
|
|
53
|
+
form,
|
|
54
|
+
}));
|
|
55
|
+
expect(result.current.mode).toBe('edit');
|
|
56
|
+
expect(result.current.isEditMode).toBe(true);
|
|
57
|
+
expect(result.current.isCreateMode).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('Data Fetching (Edit Mode)', () => {
|
|
61
|
+
it('should fetch data in edit mode', async () => {
|
|
62
|
+
const mockData = { name: 'John', email: 'john@example.com' };
|
|
63
|
+
const fetchFn = vi.fn().mockResolvedValue(mockData);
|
|
64
|
+
const form = createMockForm();
|
|
65
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
66
|
+
id: '123',
|
|
67
|
+
form,
|
|
68
|
+
fetchFn,
|
|
69
|
+
}));
|
|
70
|
+
expect(result.current.isLoading).toBe(true);
|
|
71
|
+
expect(fetchFn).toHaveBeenCalledWith('123');
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(result.current.isLoading).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
expect(form.reset).toHaveBeenCalledWith(mockData);
|
|
76
|
+
expect(result.current.originalData).toEqual(mockData);
|
|
77
|
+
expect(result.current.error).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
it('should call onFetchSuccess when data is loaded', async () => {
|
|
80
|
+
const mockData = { name: 'John' };
|
|
81
|
+
const fetchFn = vi.fn().mockResolvedValue(mockData);
|
|
82
|
+
const onFetchSuccess = vi.fn();
|
|
83
|
+
const form = createMockForm();
|
|
84
|
+
renderHook(() => useCRUDForm({
|
|
85
|
+
id: '123',
|
|
86
|
+
form,
|
|
87
|
+
fetchFn,
|
|
88
|
+
onFetchSuccess,
|
|
89
|
+
}));
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(onFetchSuccess).toHaveBeenCalledWith(mockData);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it('should handle fetch errors', async () => {
|
|
95
|
+
const error = new Error('Failed to fetch');
|
|
96
|
+
const fetchFn = vi.fn().mockRejectedValue(error);
|
|
97
|
+
const onError = vi.fn();
|
|
98
|
+
const form = createMockForm();
|
|
99
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
100
|
+
id: '123',
|
|
101
|
+
form,
|
|
102
|
+
fetchFn,
|
|
103
|
+
onError,
|
|
104
|
+
}));
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(result.current.isLoading).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
expect(result.current.error).toEqual(error);
|
|
109
|
+
expect(onError).toHaveBeenCalledWith(error, 'edit');
|
|
110
|
+
});
|
|
111
|
+
it('should not fetch data in create mode', () => {
|
|
112
|
+
const fetchFn = vi.fn();
|
|
113
|
+
const form = createMockForm();
|
|
114
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
115
|
+
form,
|
|
116
|
+
fetchFn,
|
|
117
|
+
}));
|
|
118
|
+
expect(fetchFn).not.toHaveBeenCalled();
|
|
119
|
+
expect(result.current.isLoading).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('Create Mode Submit', () => {
|
|
123
|
+
it('should call createFn with form data', async () => {
|
|
124
|
+
const formData = { name: 'John', email: 'john@example.com' };
|
|
125
|
+
const createFn = vi.fn().mockResolvedValue({ id: '123', ...formData });
|
|
126
|
+
const form = createMockForm(formData);
|
|
127
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
128
|
+
form,
|
|
129
|
+
createFn,
|
|
130
|
+
}));
|
|
131
|
+
await act(async () => {
|
|
132
|
+
await result.current.submit(formData);
|
|
133
|
+
});
|
|
134
|
+
expect(createFn).toHaveBeenCalledWith(formData);
|
|
135
|
+
});
|
|
136
|
+
it('should call onSuccess after successful create', async () => {
|
|
137
|
+
const formData = { name: 'John' };
|
|
138
|
+
const createFn = vi.fn().mockResolvedValue({ id: '123' });
|
|
139
|
+
const onSuccess = vi.fn();
|
|
140
|
+
const form = createMockForm(formData);
|
|
141
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
142
|
+
form,
|
|
143
|
+
createFn,
|
|
144
|
+
onSuccess,
|
|
145
|
+
}));
|
|
146
|
+
await act(async () => {
|
|
147
|
+
await result.current.submit(formData);
|
|
148
|
+
});
|
|
149
|
+
expect(onSuccess).toHaveBeenCalledWith(formData, 'create');
|
|
150
|
+
});
|
|
151
|
+
it('should handle create errors', async () => {
|
|
152
|
+
const error = new Error('Failed to create');
|
|
153
|
+
const createFn = vi.fn().mockRejectedValue(error);
|
|
154
|
+
const onError = vi.fn();
|
|
155
|
+
const form = createMockForm();
|
|
156
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
157
|
+
form,
|
|
158
|
+
createFn,
|
|
159
|
+
onError,
|
|
160
|
+
}));
|
|
161
|
+
await act(async () => {
|
|
162
|
+
try {
|
|
163
|
+
await result.current.submit({});
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
// Expected error
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
expect(result.current.error).toEqual(error);
|
|
170
|
+
expect(onError).toHaveBeenCalledWith(error, 'create');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('Edit Mode Submit', () => {
|
|
174
|
+
it('should call updateFn with id and form data', async () => {
|
|
175
|
+
const formData = { name: 'John Updated' };
|
|
176
|
+
const updateFn = vi.fn().mockResolvedValue({ id: '123', ...formData });
|
|
177
|
+
const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
|
|
178
|
+
const form = createMockForm(formData);
|
|
179
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
180
|
+
id: '123',
|
|
181
|
+
form,
|
|
182
|
+
fetchFn,
|
|
183
|
+
updateFn,
|
|
184
|
+
}));
|
|
185
|
+
await waitFor(() => {
|
|
186
|
+
expect(result.current.isLoading).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
await act(async () => {
|
|
189
|
+
await result.current.submit(formData);
|
|
190
|
+
});
|
|
191
|
+
expect(updateFn).toHaveBeenCalledWith('123', formData);
|
|
192
|
+
});
|
|
193
|
+
it('should call onSuccess after successful update', async () => {
|
|
194
|
+
const formData = { name: 'John Updated' };
|
|
195
|
+
const updateFn = vi.fn().mockResolvedValue({ id: '123' });
|
|
196
|
+
const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
|
|
197
|
+
const onSuccess = vi.fn();
|
|
198
|
+
const form = createMockForm(formData);
|
|
199
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
200
|
+
id: '123',
|
|
201
|
+
form,
|
|
202
|
+
fetchFn,
|
|
203
|
+
updateFn,
|
|
204
|
+
onSuccess,
|
|
205
|
+
}));
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(result.current.isLoading).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
await act(async () => {
|
|
210
|
+
await result.current.submit(formData);
|
|
211
|
+
});
|
|
212
|
+
expect(onSuccess).toHaveBeenCalledWith(formData, 'edit');
|
|
213
|
+
});
|
|
214
|
+
it('should handle update errors', async () => {
|
|
215
|
+
const error = new Error('Failed to update');
|
|
216
|
+
const updateFn = vi.fn().mockRejectedValue(error);
|
|
217
|
+
const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
|
|
218
|
+
const onError = vi.fn();
|
|
219
|
+
const form = createMockForm();
|
|
220
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
221
|
+
id: '123',
|
|
222
|
+
form,
|
|
223
|
+
fetchFn,
|
|
224
|
+
updateFn,
|
|
225
|
+
onError,
|
|
226
|
+
}));
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(result.current.isLoading).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
await act(async () => {
|
|
231
|
+
try {
|
|
232
|
+
await result.current.submit({});
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
// Expected error
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
expect(result.current.error).toEqual(error);
|
|
239
|
+
expect(onError).toHaveBeenCalledWith(error, 'edit');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('Data Transformation', () => {
|
|
243
|
+
it('should transform data before submit', async () => {
|
|
244
|
+
const formData = { tags: 'react, typescript, hooks' };
|
|
245
|
+
const transformData = vi.fn((data, mode) => ({
|
|
246
|
+
...data,
|
|
247
|
+
tags: data.tags.split(',').map((t) => t.trim()),
|
|
248
|
+
}));
|
|
249
|
+
const createFn = vi.fn().mockResolvedValue({});
|
|
250
|
+
const form = createMockForm(formData);
|
|
251
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
252
|
+
form,
|
|
253
|
+
createFn,
|
|
254
|
+
transformData,
|
|
255
|
+
}));
|
|
256
|
+
await act(async () => {
|
|
257
|
+
await result.current.submit(formData);
|
|
258
|
+
});
|
|
259
|
+
expect(transformData).toHaveBeenCalledWith(formData, 'create');
|
|
260
|
+
expect(createFn).toHaveBeenCalledWith({
|
|
261
|
+
tags: ['react', 'typescript', 'hooks'],
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
describe('Delete Operation', () => {
|
|
266
|
+
it('should delete data in edit mode', async () => {
|
|
267
|
+
const deleteFn = vi.fn().mockResolvedValue({});
|
|
268
|
+
const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
|
|
269
|
+
const form = createMockForm();
|
|
270
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
271
|
+
id: '123',
|
|
272
|
+
form,
|
|
273
|
+
fetchFn,
|
|
274
|
+
deleteFn,
|
|
275
|
+
}));
|
|
276
|
+
await waitFor(() => {
|
|
277
|
+
expect(result.current.isLoading).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
await act(async () => {
|
|
280
|
+
await result.current.handleDelete();
|
|
281
|
+
});
|
|
282
|
+
expect(deleteFn).toHaveBeenCalledWith('123');
|
|
283
|
+
expect(result.current.isDeleting).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
it('should call onSuccess after successful delete', async () => {
|
|
286
|
+
const deleteFn = vi.fn().mockResolvedValue({});
|
|
287
|
+
const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
|
|
288
|
+
const onSuccess = vi.fn();
|
|
289
|
+
const form = createMockForm();
|
|
290
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
291
|
+
id: '123',
|
|
292
|
+
form,
|
|
293
|
+
fetchFn,
|
|
294
|
+
deleteFn,
|
|
295
|
+
onSuccess,
|
|
296
|
+
}));
|
|
297
|
+
await waitFor(() => {
|
|
298
|
+
expect(result.current.isLoading).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
await act(async () => {
|
|
301
|
+
await result.current.handleDelete();
|
|
302
|
+
});
|
|
303
|
+
expect(onSuccess).toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
it('should throw error when delete is called in create mode', async () => {
|
|
306
|
+
const form = createMockForm();
|
|
307
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
308
|
+
form,
|
|
309
|
+
}));
|
|
310
|
+
await expect(async () => {
|
|
311
|
+
await act(async () => {
|
|
312
|
+
await result.current.handleDelete();
|
|
313
|
+
});
|
|
314
|
+
}).rejects.toThrow('Delete is only available in edit mode');
|
|
315
|
+
});
|
|
316
|
+
it('should handle delete errors', async () => {
|
|
317
|
+
const error = new Error('Failed to delete');
|
|
318
|
+
const deleteFn = vi.fn().mockRejectedValue(error);
|
|
319
|
+
const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
|
|
320
|
+
const onError = vi.fn();
|
|
321
|
+
const form = createMockForm();
|
|
322
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
323
|
+
id: '123',
|
|
324
|
+
form,
|
|
325
|
+
fetchFn,
|
|
326
|
+
deleteFn,
|
|
327
|
+
onError,
|
|
328
|
+
}));
|
|
329
|
+
await waitFor(() => {
|
|
330
|
+
expect(result.current.isLoading).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
await act(async () => {
|
|
333
|
+
try {
|
|
334
|
+
await result.current.handleDelete();
|
|
335
|
+
}
|
|
336
|
+
catch (e) {
|
|
337
|
+
// Expected error
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
expect(result.current.error).toEqual(error);
|
|
341
|
+
expect(onError).toHaveBeenCalledWith(error, 'edit');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
describe('Loading States', () => {
|
|
345
|
+
it('should track isSubmitting state', async () => {
|
|
346
|
+
const createFn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({}), 100)));
|
|
347
|
+
const form = createMockForm();
|
|
348
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
349
|
+
form,
|
|
350
|
+
createFn,
|
|
351
|
+
}));
|
|
352
|
+
expect(result.current.isSubmitting).toBe(false);
|
|
353
|
+
const submitPromise = act(async () => {
|
|
354
|
+
await result.current.submit({});
|
|
355
|
+
});
|
|
356
|
+
await waitFor(() => {
|
|
357
|
+
expect(result.current.isSubmitting).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
await submitPromise;
|
|
360
|
+
});
|
|
361
|
+
it('should track isDeleting state', async () => {
|
|
362
|
+
const deleteFn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({}), 100)));
|
|
363
|
+
const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
|
|
364
|
+
const form = createMockForm();
|
|
365
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
366
|
+
id: '123',
|
|
367
|
+
form,
|
|
368
|
+
fetchFn,
|
|
369
|
+
deleteFn,
|
|
370
|
+
}));
|
|
371
|
+
await waitFor(() => {
|
|
372
|
+
expect(result.current.isLoading).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
expect(result.current.isDeleting).toBe(false);
|
|
375
|
+
const deletePromise = act(async () => {
|
|
376
|
+
await result.current.handleDelete();
|
|
377
|
+
});
|
|
378
|
+
await waitFor(() => {
|
|
379
|
+
expect(result.current.isDeleting).toBe(false);
|
|
380
|
+
});
|
|
381
|
+
await deletePromise;
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
describe('Send Only Dirty Fields', () => {
|
|
385
|
+
it('should send only changed fields when sendOnlyDirtyFields is true', async () => {
|
|
386
|
+
const originalData = { name: 'John', email: 'john@example.com', age: 30 };
|
|
387
|
+
const updatedData = { name: 'John Smith', email: 'john@example.com', age: 30 };
|
|
388
|
+
const fetchFn = vi.fn().mockResolvedValue(originalData);
|
|
389
|
+
const updateFn = vi.fn().mockResolvedValue({});
|
|
390
|
+
const form = createMockForm(updatedData);
|
|
391
|
+
// Mock dirtyFields - only name is dirty
|
|
392
|
+
form.formState.dirtyFields = { name: true };
|
|
393
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
394
|
+
id: '123',
|
|
395
|
+
form,
|
|
396
|
+
fetchFn,
|
|
397
|
+
updateFn,
|
|
398
|
+
sendOnlyDirtyFields: true,
|
|
399
|
+
}));
|
|
400
|
+
await waitFor(() => {
|
|
401
|
+
expect(result.current.isLoading).toBe(false);
|
|
402
|
+
});
|
|
403
|
+
await act(async () => {
|
|
404
|
+
await result.current.submit(updatedData);
|
|
405
|
+
});
|
|
406
|
+
// Only the dirty field should be sent
|
|
407
|
+
expect(updateFn).toHaveBeenCalledWith('123', { name: 'John Smith' });
|
|
408
|
+
});
|
|
409
|
+
it('should send all fields when sendOnlyDirtyFields is false (default)', async () => {
|
|
410
|
+
const originalData = { name: 'John', email: 'john@example.com' };
|
|
411
|
+
const updatedData = { name: 'John Smith', email: 'john@example.com' };
|
|
412
|
+
const fetchFn = vi.fn().mockResolvedValue(originalData);
|
|
413
|
+
const updateFn = vi.fn().mockResolvedValue({});
|
|
414
|
+
const form = createMockForm(updatedData);
|
|
415
|
+
form.formState.dirtyFields = { name: true };
|
|
416
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
417
|
+
id: '123',
|
|
418
|
+
form,
|
|
419
|
+
fetchFn,
|
|
420
|
+
updateFn,
|
|
421
|
+
sendOnlyDirtyFields: false, // explicitly false
|
|
422
|
+
}));
|
|
423
|
+
await waitFor(() => {
|
|
424
|
+
expect(result.current.isLoading).toBe(false);
|
|
425
|
+
});
|
|
426
|
+
await act(async () => {
|
|
427
|
+
await result.current.submit(updatedData);
|
|
428
|
+
});
|
|
429
|
+
// All fields should be sent
|
|
430
|
+
expect(updateFn).toHaveBeenCalledWith('123', updatedData);
|
|
431
|
+
});
|
|
432
|
+
it('should send all fields in create mode even when sendOnlyDirtyFields is true', async () => {
|
|
433
|
+
const formData = { name: 'John', email: 'john@example.com' };
|
|
434
|
+
const createFn = vi.fn().mockResolvedValue({ id: '123' });
|
|
435
|
+
const form = createMockForm(formData);
|
|
436
|
+
form.formState.dirtyFields = { name: true, email: true };
|
|
437
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
438
|
+
form,
|
|
439
|
+
createFn,
|
|
440
|
+
sendOnlyDirtyFields: true, // Should be ignored in create mode
|
|
441
|
+
}));
|
|
442
|
+
await act(async () => {
|
|
443
|
+
await result.current.submit(formData);
|
|
444
|
+
});
|
|
445
|
+
// All fields should be sent in create mode
|
|
446
|
+
expect(createFn).toHaveBeenCalledWith(formData);
|
|
447
|
+
});
|
|
448
|
+
it('should handle nested dirty fields', async () => {
|
|
449
|
+
const originalData = {
|
|
450
|
+
name: 'John',
|
|
451
|
+
address: {
|
|
452
|
+
city: 'Seoul',
|
|
453
|
+
country: 'Korea',
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
const updatedData = {
|
|
457
|
+
name: 'John',
|
|
458
|
+
address: {
|
|
459
|
+
city: 'Busan',
|
|
460
|
+
country: 'Korea',
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
const fetchFn = vi.fn().mockResolvedValue(originalData);
|
|
464
|
+
const updateFn = vi.fn().mockResolvedValue({});
|
|
465
|
+
const form = createMockForm(updatedData);
|
|
466
|
+
// Only address.city is dirty
|
|
467
|
+
form.formState.dirtyFields = { address: { city: true } };
|
|
468
|
+
const { result } = renderHook(() => useCRUDForm({
|
|
469
|
+
id: '123',
|
|
470
|
+
form,
|
|
471
|
+
fetchFn,
|
|
472
|
+
updateFn,
|
|
473
|
+
sendOnlyDirtyFields: true,
|
|
474
|
+
}));
|
|
475
|
+
await waitFor(() => {
|
|
476
|
+
expect(result.current.isLoading).toBe(false);
|
|
477
|
+
});
|
|
478
|
+
await act(async () => {
|
|
479
|
+
await result.current.submit(updatedData);
|
|
480
|
+
});
|
|
481
|
+
// Only the nested dirty field should be extracted
|
|
482
|
+
expect(updateFn).toHaveBeenCalledWith('123', {
|
|
483
|
+
address: { city: 'Busan' },
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/form/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,eAAe,CAAC"}
|