@musecat/functionkit 1.0.0 → 1.0.2

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.
Files changed (157) hide show
  1. package/.agents/references/components/ScrolltoTop.md +36 -0
  2. package/.agents/references/components/SwitchCase.md +47 -0
  3. package/.agents/references/components/ViewportPortal.md +34 -0
  4. package/.agents/references/cookie/cookie.md +47 -0
  5. package/.agents/references/datetime/dateTime.client.md +42 -0
  6. package/.agents/references/datetime/dateTime.server.md +42 -0
  7. package/.agents/references/datetime/dateTime.shared.md +86 -0
  8. package/.agents/references/hooks/useAvoidKeyboard.md +29 -0
  9. package/.agents/references/hooks/useCheckInvisible.md +36 -0
  10. package/.agents/references/hooks/useCheckScroll.md +36 -0
  11. package/.agents/references/hooks/useClientDateTime.md +39 -0
  12. package/.agents/references/hooks/useDebounce.md +46 -0
  13. package/.agents/references/hooks/useDebouncedCallback.md +34 -0
  14. package/.agents/references/hooks/useDoubleClick.md +40 -0
  15. package/.agents/references/hooks/useGeolocation.md +37 -0
  16. package/.agents/references/hooks/useHasMounted.md +27 -0
  17. package/.agents/references/hooks/useIntersectionObserver.md +37 -0
  18. package/.agents/references/hooks/useInterval.md +40 -0
  19. package/.agents/references/hooks/useKeyboardHeight.md +33 -0
  20. package/.agents/references/hooks/useKeyboardListNavigation.md +58 -0
  21. package/.agents/references/hooks/useLongPress.md +48 -0
  22. package/.agents/references/hooks/usePreservedCallback.md +33 -0
  23. package/.agents/references/hooks/usePreservedReference.md +34 -0
  24. package/.agents/references/hooks/useRefEffect.md +35 -0
  25. package/.agents/references/hooks/useRelativeDateTime.md +35 -0
  26. package/.agents/references/hooks/useTimeout.md +42 -0
  27. package/.agents/references/hooks/useToggleState.md +39 -0
  28. package/.agents/references/hooks/useViewportHeight.md +26 -0
  29. package/.agents/references/hooks/useViewportMatch.md +32 -0
  30. package/.agents/references/utils/browserStorage.md +41 -0
  31. package/.agents/references/utils/buildContext.md +47 -0
  32. package/.agents/references/utils/clipboardShare.md +59 -0
  33. package/.agents/references/utils/floatingMotion.md +67 -0
  34. package/.agents/references/utils/getDeviceInfo.md +43 -0
  35. package/.agents/references/utils/isEditableKeyboardTarget.md +35 -0
  36. package/.agents/references/utils/mergeRefs.md +29 -0
  37. package/.agents/references/utils/seen.md +37 -0
  38. package/.agents/references/utils/subscribeKeyboardHeight.md +35 -0
  39. package/AGENTS.md +103 -396
  40. package/LICENSE +9 -0
  41. package/README.md +38 -0
  42. package/index.ts +97 -102
  43. package/package.json +27 -13
  44. package/{components → packages/components}/ScrolltoTop.tsx +3 -3
  45. package/{components → packages/components}/SwitchCase.tsx +7 -7
  46. package/packages/components/ViewportPortal.tsx +40 -0
  47. package/packages/cookie/cookie.shared.ts +138 -0
  48. package/packages/datetime/dateTime.client.ts +108 -0
  49. package/packages/datetime/dateTime.server.ts +108 -0
  50. package/packages/datetime/dateTime.shared.ts +358 -0
  51. package/packages/hooks/useAvoidKeyboard.ts +28 -0
  52. package/packages/hooks/useCheckInvisible.ts +25 -0
  53. package/packages/hooks/useCheckScroll.ts +19 -0
  54. package/packages/hooks/useClientDateTime.ts +49 -0
  55. package/packages/hooks/useDebounce.ts +118 -0
  56. package/packages/hooks/useDebouncedCallback.ts +69 -0
  57. package/packages/hooks/useDoubleClick.ts +53 -0
  58. package/packages/hooks/useGeolocation.ts +146 -0
  59. package/packages/hooks/useHasMounted.ts +13 -0
  60. package/packages/hooks/useIntersectionObserver.ts +34 -0
  61. package/packages/hooks/useInterval.ts +56 -0
  62. package/packages/hooks/useKeyboardHeight.ts +18 -0
  63. package/packages/hooks/useKeyboardListNavigation.ts +170 -0
  64. package/packages/hooks/useLongPress.ts +120 -0
  65. package/packages/hooks/usePreservedCallback.ts +21 -0
  66. package/packages/hooks/usePreservedReference.ts +21 -0
  67. package/packages/hooks/useRefEffect.ts +35 -0
  68. package/packages/hooks/useRelativeDateTime.ts +54 -0
  69. package/packages/hooks/useTimeout.ts +56 -0
  70. package/packages/hooks/useToggleState.ts +13 -0
  71. package/packages/hooks/useViewportHeight.ts +23 -0
  72. package/packages/hooks/useViewportMatch.ts +27 -0
  73. package/packages/utils/browserStorage.ts +59 -0
  74. package/packages/utils/buildContext.tsx +19 -0
  75. package/packages/utils/checkDevice.ts +74 -0
  76. package/packages/utils/clipboardShare.tsx +47 -0
  77. package/{utils → packages/utils}/clipboardShare.types.ts +7 -7
  78. package/packages/utils/floatingMotion.ts +99 -0
  79. package/packages/utils/keyboardTarget.ts +13 -0
  80. package/packages/utils/mergeRefs.ts +15 -0
  81. package/packages/utils/seen.ts +29 -0
  82. package/packages/utils/subscribeKeyboardHeight.ts +54 -0
  83. package/tests/components/ScrolltoTop.test.tsx +15 -0
  84. package/tests/components/SwitchCase.test.tsx +34 -0
  85. package/tests/components/ViewportPortal.test.tsx +42 -0
  86. package/tests/cookie/cookie.test.ts +156 -0
  87. package/tests/datetime/datetime.test.ts +250 -0
  88. package/tests/hooks/useAvoidKeyboard.test.ts +12 -0
  89. package/tests/hooks/useCheckInvisible.test.ts +28 -0
  90. package/tests/hooks/useCheckScroll.test.ts +23 -0
  91. package/tests/hooks/useClientDateTime.test.ts +14 -0
  92. package/tests/hooks/useDebounce.test.ts +71 -0
  93. package/tests/hooks/useDebouncedCallback.test.ts +34 -0
  94. package/tests/hooks/useDoubleClick.test.ts +31 -0
  95. package/tests/hooks/useGeolocation.test.ts +50 -0
  96. package/tests/hooks/useHasMounted.test.ts +10 -0
  97. package/tests/hooks/useIntersectionObserver.test.ts +13 -0
  98. package/tests/hooks/useInterval.test.ts +31 -0
  99. package/tests/hooks/useKeyboardHeight.test.ts +10 -0
  100. package/tests/hooks/useKeyboardListNavigation.test.ts +82 -0
  101. package/tests/hooks/useLongPress.test.ts +52 -0
  102. package/tests/hooks/usePreservedCallback.test.ts +35 -0
  103. package/tests/hooks/usePreservedReference.test.ts +35 -0
  104. package/tests/hooks/useRefEffect.test.ts +30 -0
  105. package/tests/hooks/useRelativeDateTime.test.ts +19 -0
  106. package/tests/hooks/useTimeout.test.ts +34 -0
  107. package/tests/hooks/useToggleState.test.ts +35 -0
  108. package/tests/hooks/useViewportHeight.test.ts +10 -0
  109. package/tests/hooks/useViewportMatch.test.ts +35 -0
  110. package/tests/setup.ts +9 -0
  111. package/tests/utils/browserStorage.test.ts +54 -0
  112. package/tests/utils/buildContext.test.tsx +27 -0
  113. package/tests/utils/checkDevice.test.ts +84 -0
  114. package/tests/utils/clipboardShare.test.ts +78 -0
  115. package/tests/utils/floatingMotion.test.ts +84 -0
  116. package/tests/utils/keyboardTarget.test.ts +44 -0
  117. package/tests/utils/mergeRefs.test.ts +18 -0
  118. package/tests/utils/seen.test.ts +31 -0
  119. package/tests/utils/subscribeKeyboardHeight.test.ts +50 -0
  120. package/tsconfig.json +16 -16
  121. package/vitest.config.ts +16 -0
  122. package/components/ViewportPortal.tsx +0 -40
  123. package/cookie/cookie.shared.ts +0 -142
  124. package/datetime/dateTime.client.ts +0 -108
  125. package/datetime/dateTime.server.ts +0 -108
  126. package/datetime/dateTime.shared.ts +0 -358
  127. package/hooks/useAvoidKeyboard.ts +0 -28
  128. package/hooks/useCheckInvisible.ts +0 -25
  129. package/hooks/useCheckScroll.ts +0 -19
  130. package/hooks/useClientDateTime.ts +0 -49
  131. package/hooks/useDebounce.ts +0 -118
  132. package/hooks/useDebouncedCallback.ts +0 -69
  133. package/hooks/useDoubleClick.ts +0 -53
  134. package/hooks/useGeolocation.ts +0 -146
  135. package/hooks/useHasMounted.ts +0 -13
  136. package/hooks/useIntersectionObserver.ts +0 -34
  137. package/hooks/useInterval.ts +0 -55
  138. package/hooks/useKeyboardHeight.ts +0 -18
  139. package/hooks/useKeyboardListNavigation.ts +0 -170
  140. package/hooks/useLongPress.ts +0 -120
  141. package/hooks/usePreservedCallback.ts +0 -21
  142. package/hooks/usePreservedReference.ts +0 -21
  143. package/hooks/useRefEffect.ts +0 -35
  144. package/hooks/useRelativeDateTime.ts +0 -54
  145. package/hooks/useTimeout.ts +0 -56
  146. package/hooks/useToggleState.ts +0 -13
  147. package/hooks/useViewportHeight.ts +0 -23
  148. package/hooks/useViewportMatch.ts +0 -33
  149. package/utils/browserStorage.ts +0 -59
  150. package/utils/buildContext.tsx +0 -19
  151. package/utils/checkDevice.ts +0 -74
  152. package/utils/clipboardShare.tsx +0 -47
  153. package/utils/floatingMotion.ts +0 -99
  154. package/utils/keyboardTarget.ts +0 -13
  155. package/utils/mergeRefs.ts +0 -15
  156. package/utils/seen.ts +0 -29
  157. package/utils/subscribeKeyboardHeight.ts +0 -54
@@ -0,0 +1,50 @@
1
+ import { describe, test, expect, vi, beforeEach, type Mock } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useGeolocation } from "@/packages/hooks/useGeolocation";
4
+
5
+ describe("useGeolocation", () => {
6
+ let mockGeolocation: {
7
+ getCurrentPosition: Mock;
8
+ watchPosition: Mock;
9
+ clearWatch: Mock;
10
+ };
11
+
12
+ beforeEach(() => {
13
+ mockGeolocation = {
14
+ getCurrentPosition: vi.fn((success) =>
15
+ success({
16
+ coords: {
17
+ latitude: 37.5665,
18
+ longitude: 126.978,
19
+ accuracy: 10,
20
+ altitude: null,
21
+ altitudeAccuracy: null,
22
+ heading: null,
23
+ speed: null,
24
+ },
25
+ timestamp: Date.now(),
26
+ }),
27
+ ),
28
+ watchPosition: vi.fn(),
29
+ clearWatch: vi.fn(),
30
+ };
31
+ Object.defineProperty(navigator, "geolocation", {
32
+ value: mockGeolocation,
33
+ writable: true,
34
+ configurable: true,
35
+ });
36
+ });
37
+
38
+ test("returns default state", () => {
39
+ const { result } = renderHook(() => useGeolocation());
40
+ expect(result.current.loading).toBe(false);
41
+ expect(result.current.error).toBeNull();
42
+ expect(result.current.data).toBeNull();
43
+ });
44
+
45
+ test("getCurrentPosition fetches location", () => {
46
+ const { result } = renderHook(() => useGeolocation());
47
+ act(() => { result.current.getCurrentPosition(); });
48
+ expect(mockGeolocation.getCurrentPosition).toHaveBeenCalledOnce();
49
+ });
50
+ });
@@ -0,0 +1,10 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useHasMounted } from "@/packages/hooks/useHasMounted";
4
+
5
+ describe("useHasMounted", () => {
6
+ test("returns true after mount", () => {
7
+ const { result } = renderHook(() => useHasMounted());
8
+ expect(result.current).toBe(true);
9
+ });
10
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useIntersectionObserver } from "@/packages/hooks/useIntersectionObserver";
4
+
5
+ describe("useIntersectionObserver", () => {
6
+ test("returns a ref callback", () => {
7
+ const callback = vi.fn();
8
+ const { result } = renderHook(() =>
9
+ useIntersectionObserver(callback, { threshold: 0 }),
10
+ );
11
+ expect(typeof result.current).toBe("function");
12
+ });
13
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useInterval } from "@/packages/hooks/useInterval";
4
+
5
+ describe("useInterval", () => {
6
+ beforeEach(() => { vi.useFakeTimers(); });
7
+ afterEach(() => { vi.useRealTimers(); });
8
+
9
+ test("calls callback repeatedly at interval", () => {
10
+ const fn = vi.fn();
11
+ renderHook(() => useInterval(fn, 100));
12
+ expect(fn).not.toHaveBeenCalled();
13
+ vi.advanceTimersByTime(100);
14
+ expect(fn).toHaveBeenCalledTimes(1);
15
+ vi.advanceTimersByTime(100);
16
+ expect(fn).toHaveBeenCalledTimes(2);
17
+ });
18
+
19
+ test("calls immediately with immediate option", () => {
20
+ const fn = vi.fn();
21
+ renderHook(() => useInterval(fn, { delay: 100, immediate: true }));
22
+ expect(fn).toHaveBeenCalledOnce();
23
+ });
24
+
25
+ test("does not call when disabled", () => {
26
+ const fn = vi.fn();
27
+ renderHook(() => useInterval(fn, { delay: 100, enabled: false }));
28
+ vi.advanceTimersByTime(200);
29
+ expect(fn).not.toHaveBeenCalled();
30
+ });
31
+ });
@@ -0,0 +1,10 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useKeyboardHeight } from "@/packages/hooks/useKeyboardHeight";
4
+
5
+ describe("useKeyboardHeight", () => {
6
+ test("returns default keyboard height of 0", () => {
7
+ const { result } = renderHook(() => useKeyboardHeight());
8
+ expect(result.current.keyboardHeight).toBe(0);
9
+ });
10
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useKeyboardListNavigation } from "@/packages/hooks/useKeyboardListNavigation";
4
+
5
+ describe("useKeyboardListNavigation", () => {
6
+ test("initializes with activeIndex -1", () => {
7
+ const { result } = renderHook(() =>
8
+ useKeyboardListNavigation({ itemCount: 3 }),
9
+ );
10
+ expect(result.current.activeIndex).toBe(-1);
11
+ });
12
+
13
+ test("handleListKeyDown with ArrowDown moves to first item when none active", () => {
14
+ const { result } = renderHook(() =>
15
+ useKeyboardListNavigation({ itemCount: 3 }),
16
+ );
17
+
18
+ const container = document.createElement("div");
19
+ const items = [0, 1, 2].map(() => {
20
+ const el = document.createElement("div");
21
+ el.setAttribute("tabindex", "-1");
22
+ el.scrollIntoView = vi.fn();
23
+ container.appendChild(el);
24
+ return el;
25
+ });
26
+ items.forEach((el, i) => result.current.setItemRef(i, el));
27
+
28
+ const event = {
29
+ key: "ArrowDown",
30
+ defaultPrevented: false,
31
+ preventDefault: vi.fn(),
32
+ target: container,
33
+ } as unknown as React.KeyboardEvent<HTMLElement>;
34
+
35
+ act(() => { result.current.handleListKeyDown(event); });
36
+ expect(result.current.activeIndex).toBe(0);
37
+ });
38
+
39
+ test("handleListKeyDown with ArrowUp moves to previous item", () => {
40
+ const { result } = renderHook(() =>
41
+ useKeyboardListNavigation({ itemCount: 3 }),
42
+ );
43
+
44
+ const container = document.createElement("div");
45
+ const items = [0, 1, 2].map(() => {
46
+ const el = document.createElement("div");
47
+ el.setAttribute("tabindex", "-1");
48
+ el.scrollIntoView = vi.fn();
49
+ container.appendChild(el);
50
+ return el;
51
+ });
52
+ items.forEach((el, i) => result.current.setItemRef(i, el));
53
+ act(() => { result.current.setActiveIndex(1); });
54
+
55
+ const event = {
56
+ key: "ArrowUp",
57
+ defaultPrevented: false,
58
+ preventDefault: vi.fn(),
59
+ target: container,
60
+ } as unknown as React.KeyboardEvent<HTMLElement>;
61
+
62
+ act(() => { result.current.handleListKeyDown(event); });
63
+ expect(result.current.activeIndex).toBe(0);
64
+ });
65
+
66
+ test("Escape resets activeIndex to -1", () => {
67
+ const { result } = renderHook(() =>
68
+ useKeyboardListNavigation({ itemCount: 3 }),
69
+ );
70
+
71
+ const event = {
72
+ key: "Escape",
73
+ defaultPrevented: false,
74
+ preventDefault: vi.fn(),
75
+ target: document.createElement("div"),
76
+ } as unknown as React.KeyboardEvent<HTMLElement>;
77
+
78
+ result.current.setActiveIndex(1);
79
+ act(() => { result.current.handleListKeyDown(event); });
80
+ expect(result.current.activeIndex).toBe(-1);
81
+ });
82
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import type React from "react";
4
+ import { useLongPress } from "@/packages/hooks/useLongPress";
5
+
6
+ function createMouseEvent(type: string): React.MouseEvent<HTMLElement> {
7
+ return {
8
+ type,
9
+ nativeEvent: new MouseEvent(type, { clientX: 0, clientY: 0 }),
10
+ clientX: 0,
11
+ clientY: 0,
12
+ } as unknown as React.MouseEvent<HTMLElement>;
13
+ }
14
+
15
+ describe("useLongPress", () => {
16
+ beforeEach(() => { vi.useFakeTimers(); });
17
+ afterEach(() => { vi.useRealTimers(); });
18
+
19
+ test("fires onLongPress after delay", () => {
20
+ const onLongPress = vi.fn();
21
+ const { result } = renderHook(() => useLongPress(onLongPress, { delay: 200 }));
22
+
23
+ act(() => { result.current.onMouseDown(createMouseEvent("mousedown")); });
24
+ expect(onLongPress).not.toHaveBeenCalled();
25
+ vi.advanceTimersByTime(200);
26
+ expect(onLongPress).toHaveBeenCalledOnce();
27
+ });
28
+
29
+ test("fires onClick when released before delay", () => {
30
+ const onLongPress = vi.fn();
31
+ const onClick = vi.fn();
32
+ const { result } = renderHook(() =>
33
+ useLongPress(onLongPress, { delay: 200, onClick }),
34
+ );
35
+
36
+ act(() => { result.current.onMouseDown(createMouseEvent("mousedown")); });
37
+ act(() => { result.current.onMouseUp(createMouseEvent("mouseup")); });
38
+ vi.advanceTimersByTime(200);
39
+ expect(onLongPress).not.toHaveBeenCalled();
40
+ expect(onClick).toHaveBeenCalledOnce();
41
+ });
42
+
43
+ test("cancels on mouse leave", () => {
44
+ const onLongPress = vi.fn();
45
+ const { result } = renderHook(() => useLongPress(onLongPress, { delay: 200 }));
46
+
47
+ act(() => { result.current.onMouseDown(createMouseEvent("mousedown")); });
48
+ act(() => { result.current.onMouseLeave(createMouseEvent("mouseleave")); });
49
+ vi.advanceTimersByTime(200);
50
+ expect(onLongPress).not.toHaveBeenCalled();
51
+ });
52
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { usePreservedCallback } from "@/packages/hooks/usePreservedCallback";
4
+
5
+ describe("usePreservedCallback", () => {
6
+ test("returns a stable function reference across renders", () => {
7
+ const { result, rerender } = renderHook(
8
+ ({ cb }) => usePreservedCallback(cb),
9
+ { initialProps: { cb: vi.fn() } },
10
+ );
11
+ const firstRef = result.current;
12
+ rerender({ cb: vi.fn() });
13
+ expect(result.current).toBe(firstRef);
14
+ });
15
+
16
+ test("invokes the latest callback", () => {
17
+ const fn1 = vi.fn();
18
+ const fn2 = vi.fn();
19
+ const { result, rerender } = renderHook(
20
+ ({ cb }) => usePreservedCallback(cb),
21
+ { initialProps: { cb: fn1 } },
22
+ );
23
+ rerender({ cb: fn2 });
24
+ result.current();
25
+ expect(fn2).toHaveBeenCalledOnce();
26
+ expect(fn1).not.toHaveBeenCalled();
27
+ });
28
+
29
+ test("passes arguments through", () => {
30
+ const fn = vi.fn();
31
+ const { result } = renderHook(() => usePreservedCallback(fn));
32
+ result.current("a", 1);
33
+ expect(fn).toHaveBeenCalledWith("a", 1);
34
+ });
35
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { usePreservedReference } from "@/packages/hooks/usePreservedReference";
4
+
5
+ describe("usePreservedReference", () => {
6
+ test("returns same reference for deeply equal values", () => {
7
+ const { result, rerender } = renderHook(
8
+ ({ val }) => usePreservedReference(val),
9
+ { initialProps: { val: { a: 1, b: { c: [2, 3] } } } },
10
+ );
11
+ const first = result.current;
12
+ rerender({ val: { a: 1, b: { c: [2, 3] } } });
13
+ expect(result.current).toBe(first);
14
+ });
15
+
16
+ test("returns new reference for different values", () => {
17
+ const { result, rerender } = renderHook(
18
+ ({ val }) => usePreservedReference(val),
19
+ { initialProps: { val: { a: 1 } } },
20
+ );
21
+ const first = result.current;
22
+ rerender({ val: { a: 2 } });
23
+ expect(result.current).not.toBe(first);
24
+ });
25
+
26
+ test("accepts custom comparator", () => {
27
+ const { result, rerender } = renderHook(
28
+ ({ val }) => usePreservedReference(val, () => true),
29
+ { initialProps: { val: { a: 1 } } },
30
+ );
31
+ const first = result.current;
32
+ rerender({ val: { a: 999 } });
33
+ expect(result.current).toBe(first);
34
+ });
35
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useRefEffect } from "@/packages/hooks/useRefEffect";
4
+
5
+ describe("useRefEffect", () => {
6
+ test("calls callback with element and cleanup on unmount", () => {
7
+ const cleanup = vi.fn();
8
+ const callback = vi.fn(() => cleanup);
9
+ const { result } = renderHook(() => useRefEffect(callback, []));
10
+
11
+ const element = document.createElement("div");
12
+ act(() => { (result.current as (el: HTMLElement | null) => void)(element); });
13
+ expect(callback).toHaveBeenCalledWith(element);
14
+
15
+ act(() => { (result.current as (el: HTMLElement | null) => void)(null); });
16
+ expect(cleanup).toHaveBeenCalledOnce();
17
+ });
18
+
19
+ test("calls cleanup before setting new element", () => {
20
+ const cleanup = vi.fn();
21
+ const callback = vi.fn(() => cleanup);
22
+ const { result } = renderHook(() => useRefEffect(callback, []));
23
+
24
+ const el1 = document.createElement("div");
25
+ const el2 = document.createElement("span");
26
+ act(() => { (result.current as (el: HTMLElement | null) => void)(el1); });
27
+ act(() => { (result.current as (el: HTMLElement | null) => void)(el2); });
28
+ expect(cleanup).toHaveBeenCalledOnce();
29
+ });
30
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useRelativeDateTime } from "@/packages/hooks/useRelativeDateTime";
4
+
5
+ describe("useRelativeDateTime", () => {
6
+ beforeEach(() => { vi.useFakeTimers(); });
7
+ afterEach(() => { vi.useRealTimers(); });
8
+
9
+ test("returns relative time text", () => {
10
+ const now = Date.now();
11
+ const fiveMinAgo = new Date(now - 5 * 60 * 1000).toISOString();
12
+ const { result } = renderHook(() =>
13
+ useRelativeDateTime(fiveMinAgo, { locale: "ko" }),
14
+ );
15
+ expect(result.current.ready).toBe(true);
16
+ expect(result.current.isRelative).toBe(true);
17
+ expect(result.current.text).toBeTruthy();
18
+ });
19
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useTimeout } from "@/packages/hooks/useTimeout";
4
+
5
+ describe("useTimeout", () => {
6
+ beforeEach(() => { vi.useFakeTimers(); });
7
+ afterEach(() => { vi.useRealTimers(); });
8
+
9
+ test("calls callback after delay", () => {
10
+ const fn = vi.fn();
11
+ renderHook(() => useTimeout(fn, 100));
12
+ expect(fn).not.toHaveBeenCalled();
13
+ vi.advanceTimersByTime(100);
14
+ expect(fn).toHaveBeenCalledOnce();
15
+ });
16
+
17
+ test("does not call when disabled", () => {
18
+ const fn = vi.fn();
19
+ renderHook(() => useTimeout(fn, 100, { enabled: false }));
20
+ vi.advanceTimersByTime(200);
21
+ expect(fn).not.toHaveBeenCalled();
22
+ });
23
+
24
+ test("start and clear controls", () => {
25
+ const fn = vi.fn();
26
+ const { result } = renderHook(() => useTimeout(fn, 1000, { enabled: false }));
27
+ expect(result.current.isPending()).toBe(false);
28
+ act(() => { result.current.start(50); });
29
+ expect(result.current.isPending()).toBe(true);
30
+ vi.advanceTimersByTime(50);
31
+ expect(fn).toHaveBeenCalledOnce();
32
+ expect(result.current.isPending()).toBe(false);
33
+ });
34
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useToggleState } from "@/packages/hooks/useToggleState";
4
+
5
+ describe("useToggleState", () => {
6
+ test("initializes with false by default", () => {
7
+ const { result } = renderHook(() => useToggleState());
8
+ expect(result.current.value).toBe(false);
9
+ });
10
+
11
+ test("initializes with provided value", () => {
12
+ const { result } = renderHook(() => useToggleState(true));
13
+ expect(result.current.value).toBe(true);
14
+ });
15
+
16
+ test("setTrue sets value to true", () => {
17
+ const { result } = renderHook(() => useToggleState(false));
18
+ act(() => { result.current.setTrue(); });
19
+ expect(result.current.value).toBe(true);
20
+ });
21
+
22
+ test("setFalse sets value to false", () => {
23
+ const { result } = renderHook(() => useToggleState(true));
24
+ act(() => { result.current.setFalse(); });
25
+ expect(result.current.value).toBe(false);
26
+ });
27
+
28
+ test("toggle switches value", () => {
29
+ const { result } = renderHook(() => useToggleState(false));
30
+ act(() => { result.current.toggle(); });
31
+ expect(result.current.value).toBe(true);
32
+ act(() => { result.current.toggle(); });
33
+ expect(result.current.value).toBe(false);
34
+ });
35
+ });
@@ -0,0 +1,10 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useViewportHeight } from "@/packages/hooks/useViewportHeight";
4
+
5
+ describe("useViewportHeight", () => {
6
+ test("returns a height value", () => {
7
+ const { result } = renderHook(() => useViewportHeight());
8
+ expect(result.current.height).toBeGreaterThanOrEqual(0);
9
+ });
10
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, test, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useViewportMatch } from "@/packages/hooks/useViewportMatch";
4
+
5
+ describe("useViewportMatch", () => {
6
+ beforeEach(() => {
7
+ vi.stubGlobal(
8
+ "matchMedia",
9
+ vi.fn().mockImplementation((query: string) => ({
10
+ matches: false,
11
+ media: query,
12
+ addEventListener: vi.fn(),
13
+ removeEventListener: vi.fn(),
14
+ })),
15
+ );
16
+ });
17
+
18
+ test("returns false initially", () => {
19
+ const { result } = renderHook(() => useViewportMatch("(min-width: 768px)"));
20
+ expect(result.current).toBe(false);
21
+ });
22
+
23
+ test("returns true when matched", () => {
24
+ vi.stubGlobal(
25
+ "matchMedia",
26
+ vi.fn().mockImplementation(() => ({
27
+ matches: true,
28
+ addEventListener: vi.fn(),
29
+ removeEventListener: vi.fn(),
30
+ })),
31
+ );
32
+ const { result } = renderHook(() => useViewportMatch("(min-width: 768px)"));
33
+ expect(result.current).toBe(true);
34
+ });
35
+ });
package/tests/setup.ts ADDED
@@ -0,0 +1,9 @@
1
+ import "@testing-library/jest-dom";
2
+
3
+ class ResizeObserverMock {
4
+ observe() {}
5
+ unobserve() {}
6
+ disconnect() {}
7
+ }
8
+
9
+ window.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver;
@@ -0,0 +1,54 @@
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ getLocalStorage,
4
+ updateLocalStorage,
5
+ removeLocalStorage,
6
+ getSessionStorage,
7
+ updateSessionStorage,
8
+ removeSessionStorage,
9
+ } from "@/packages/utils/browserStorage";
10
+
11
+ describe("browserStorage", () => {
12
+ let mockStorage: Record<string, string>;
13
+ let mockStorage2: Record<string, string>;
14
+
15
+ beforeEach(() => {
16
+ mockStorage = {};
17
+ mockStorage2 = {};
18
+
19
+ const createMockStorage = (store: Record<string, string>) => ({
20
+ getItem: vi.fn((key) => store[key] ?? null),
21
+ setItem: vi.fn((key, value) => { store[key] = value.toString(); }),
22
+ removeItem: vi.fn((key) => { delete store[key]; }),
23
+ clear: vi.fn(() => { Object.keys(store).forEach((k) => delete store[k]); }),
24
+ });
25
+
26
+ vi.stubGlobal("localStorage", createMockStorage(mockStorage));
27
+ vi.stubGlobal("sessionStorage", createMockStorage(mockStorage2));
28
+ });
29
+
30
+ afterEach(() => { vi.unstubAllGlobals(); });
31
+
32
+ test("localStorage operations", () => {
33
+ updateLocalStorage("foo", { bar: "baz" });
34
+ expect(localStorage.getItem("foo")).toBe('{"bar":"baz"}');
35
+ expect(getLocalStorage("foo")).toEqual({ bar: "baz" });
36
+
37
+ removeLocalStorage("foo");
38
+ expect(getLocalStorage("foo")).toBeNull();
39
+ });
40
+
41
+ test("sessionStorage operations", () => {
42
+ updateSessionStorage("foo", { bar: "baz" });
43
+ expect(sessionStorage.getItem("foo")).toBe('{"bar":"baz"}');
44
+ expect(getSessionStorage("foo")).toEqual({ bar: "baz" });
45
+
46
+ removeSessionStorage("foo");
47
+ expect(getSessionStorage("foo")).toBeNull();
48
+ });
49
+
50
+ test("handles invalid json gracefully", () => {
51
+ localStorage.setItem("invalid", "not-json");
52
+ expect(getLocalStorage("invalid")).toBeNull();
53
+ });
54
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import React from "react";
4
+ import { buildContext } from "@/packages/utils/buildContext";
5
+
6
+ describe("buildContext", () => {
7
+ const [Provider, useValue] = buildContext<string>("default");
8
+
9
+ function TestComponent() {
10
+ const val = useValue();
11
+ return <div data-testid="val">{val}</div>;
12
+ }
13
+
14
+ test("returns default value when not wrapped in provider", () => {
15
+ render(<TestComponent />);
16
+ expect(screen.getByTestId("val").textContent).toBe("default");
17
+ });
18
+
19
+ test("returns provided value", () => {
20
+ render(
21
+ <Provider value="provided">
22
+ <TestComponent />
23
+ </Provider>,
24
+ );
25
+ expect(screen.getByTestId("val").textContent).toBe("provided");
26
+ });
27
+ });
@@ -0,0 +1,84 @@
1
+ import { describe, test, expect, afterEach } from "vitest";
2
+ import { getDeviceInfo } from "@/packages/utils/checkDevice";
3
+
4
+ describe("getDeviceInfo", () => {
5
+ const originalUserAgent = navigator.userAgent;
6
+
7
+ afterEach(() => {
8
+ Object.defineProperty(navigator, "userAgent", {
9
+ value: originalUserAgent,
10
+ writable: true,
11
+ configurable: true,
12
+ });
13
+ });
14
+
15
+ test("returns default values in SSR (no window)", () => {
16
+ const win = global.window;
17
+ (global as any).window = undefined;
18
+ const info = getDeviceInfo();
19
+ expect(info.isMobile).toBe(false);
20
+ expect(info.browser).toBe("unknown");
21
+ global.window = win;
22
+ });
23
+
24
+ test("detects mobile Chrome", () => {
25
+ Object.defineProperty(navigator, "userAgent", {
26
+ value: "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
27
+ writable: true,
28
+ configurable: true,
29
+ });
30
+ const info = getDeviceInfo();
31
+ expect(info.isMobile).toBe(true);
32
+ expect(info.isAndroid).toBe(true);
33
+ expect(info.browser).toBe("chrome");
34
+ });
35
+
36
+ test("detects iOS Safari", () => {
37
+ Object.defineProperty(navigator, "userAgent", {
38
+ value: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
39
+ writable: true,
40
+ configurable: true,
41
+ });
42
+ const info = getDeviceInfo();
43
+ expect(info.isMobile).toBe(true);
44
+ expect(info.isIOS).toBe(true);
45
+ expect(info.isSafari).toBe(true);
46
+ expect(info.isIOSSafari).toBe(true);
47
+ expect(info.browser).toBe("safari");
48
+ });
49
+
50
+ test("detects desktop Chrome", () => {
51
+ Object.defineProperty(navigator, "userAgent", {
52
+ value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
53
+ writable: true,
54
+ configurable: true,
55
+ });
56
+ const info = getDeviceInfo();
57
+ expect(info.isMobile).toBe(false);
58
+ expect(info.browser).toBe("chrome");
59
+ });
60
+
61
+ test("detects Mac Safari", () => {
62
+ Object.defineProperty(navigator, "userAgent", {
63
+ value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/604.1",
64
+ writable: true,
65
+ configurable: true,
66
+ });
67
+ const info = getDeviceInfo();
68
+ expect(info.isMobile).toBe(false);
69
+ expect(info.isMacSafari).toBe(true);
70
+ expect(info.isSafari).toBe(true);
71
+ expect(info.browser).toBe("safari");
72
+ });
73
+
74
+ test("detects Samsung Browser", () => {
75
+ Object.defineProperty(navigator, "userAgent", {
76
+ value: "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/120.0.6099.230 Mobile Safari/537.36",
77
+ writable: true,
78
+ configurable: true,
79
+ });
80
+ const info = getDeviceInfo();
81
+ expect(info.isSamsungBrowser).toBe(true);
82
+ expect(info.browser).toBe("samsung");
83
+ });
84
+ });