@musecat/functionkit 1.0.1 → 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 (120) 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/README.md +18 -5
  41. package/index.ts +63 -68
  42. package/package.json +27 -13
  43. package/packages/components/ScrolltoTop.tsx +3 -3
  44. package/packages/components/SwitchCase.tsx +7 -7
  45. package/packages/components/ViewportPortal.tsx +23 -23
  46. package/packages/cookie/cookie.shared.ts +97 -101
  47. package/packages/datetime/dateTime.client.ts +86 -86
  48. package/packages/datetime/dateTime.server.ts +86 -86
  49. package/packages/datetime/dateTime.shared.ts +281 -281
  50. package/packages/hooks/useAvoidKeyboard.ts +15 -15
  51. package/packages/hooks/useCheckInvisible.ts +16 -16
  52. package/packages/hooks/useCheckScroll.ts +10 -10
  53. package/packages/hooks/useClientDateTime.ts +33 -33
  54. package/packages/hooks/useDebounce.ts +95 -95
  55. package/packages/hooks/useDebouncedCallback.ts +45 -45
  56. package/packages/hooks/useDoubleClick.ts +43 -43
  57. package/packages/hooks/useGeolocation.ts +130 -130
  58. package/packages/hooks/useHasMounted.ts +5 -5
  59. package/packages/hooks/useIntersectionObserver.ts +20 -20
  60. package/packages/hooks/useInterval.ts +46 -45
  61. package/packages/hooks/useKeyboardHeight.ts +9 -9
  62. package/packages/hooks/useKeyboardListNavigation.ts +156 -156
  63. package/packages/hooks/useLongPress.ts +98 -98
  64. package/packages/hooks/usePreservedCallback.ts +12 -12
  65. package/packages/hooks/usePreservedReference.ts +10 -10
  66. package/packages/hooks/useRefEffect.ts +19 -19
  67. package/packages/hooks/useRelativeDateTime.ts +37 -37
  68. package/packages/hooks/useTimeout.ts +41 -41
  69. package/packages/hooks/useToggleState.ts +5 -5
  70. package/packages/hooks/useViewportHeight.ts +13 -13
  71. package/packages/hooks/useViewportMatch.ts +16 -22
  72. package/packages/utils/browserStorage.ts +26 -26
  73. package/packages/utils/buildContext.tsx +10 -10
  74. package/packages/utils/checkDevice.ts +64 -64
  75. package/packages/utils/clipboardShare.tsx +31 -31
  76. package/packages/utils/clipboardShare.types.ts +7 -7
  77. package/packages/utils/floatingMotion.ts +71 -71
  78. package/packages/utils/keyboardTarget.ts +9 -9
  79. package/packages/utils/mergeRefs.ts +9 -9
  80. package/packages/utils/seen.ts +15 -15
  81. package/packages/utils/subscribeKeyboardHeight.ts +46 -46
  82. package/tests/components/ScrolltoTop.test.tsx +15 -0
  83. package/tests/components/SwitchCase.test.tsx +34 -0
  84. package/tests/components/ViewportPortal.test.tsx +42 -0
  85. package/tests/cookie/cookie.test.ts +156 -0
  86. package/tests/datetime/datetime.test.ts +250 -0
  87. package/tests/hooks/useAvoidKeyboard.test.ts +12 -0
  88. package/tests/hooks/useCheckInvisible.test.ts +28 -0
  89. package/tests/hooks/useCheckScroll.test.ts +23 -0
  90. package/tests/hooks/useClientDateTime.test.ts +14 -0
  91. package/tests/hooks/useDebounce.test.ts +71 -0
  92. package/tests/hooks/useDebouncedCallback.test.ts +34 -0
  93. package/tests/hooks/useDoubleClick.test.ts +31 -0
  94. package/tests/hooks/useGeolocation.test.ts +50 -0
  95. package/tests/hooks/useHasMounted.test.ts +10 -0
  96. package/tests/hooks/useIntersectionObserver.test.ts +13 -0
  97. package/tests/hooks/useInterval.test.ts +31 -0
  98. package/tests/hooks/useKeyboardHeight.test.ts +10 -0
  99. package/tests/hooks/useKeyboardListNavigation.test.ts +82 -0
  100. package/tests/hooks/useLongPress.test.ts +52 -0
  101. package/tests/hooks/usePreservedCallback.test.ts +35 -0
  102. package/tests/hooks/usePreservedReference.test.ts +35 -0
  103. package/tests/hooks/useRefEffect.test.ts +30 -0
  104. package/tests/hooks/useRelativeDateTime.test.ts +19 -0
  105. package/tests/hooks/useTimeout.test.ts +34 -0
  106. package/tests/hooks/useToggleState.test.ts +35 -0
  107. package/tests/hooks/useViewportHeight.test.ts +10 -0
  108. package/tests/hooks/useViewportMatch.test.ts +35 -0
  109. package/tests/setup.ts +9 -0
  110. package/tests/utils/browserStorage.test.ts +54 -0
  111. package/tests/utils/buildContext.test.tsx +27 -0
  112. package/tests/utils/checkDevice.test.ts +84 -0
  113. package/tests/utils/clipboardShare.test.ts +78 -0
  114. package/tests/utils/floatingMotion.test.ts +84 -0
  115. package/tests/utils/keyboardTarget.test.ts +44 -0
  116. package/tests/utils/mergeRefs.test.ts +18 -0
  117. package/tests/utils/seen.test.ts +31 -0
  118. package/tests/utils/subscribeKeyboardHeight.test.ts +50 -0
  119. package/tsconfig.json +16 -16
  120. package/vitest.config.ts +16 -0
@@ -0,0 +1,250 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ normalizeAppLocale,
4
+ toIntlLocale,
5
+ toDate,
6
+ toUtcMidnight,
7
+ parseUtcDateInput,
8
+ formatUtcDateKey,
9
+ getUtcWeekdayIndex,
10
+ addUtcDays,
11
+ formatLongDate,
12
+ formatDotDate,
13
+ format24HourTime,
14
+ formatKoreanTime,
15
+ formatTwelveHourTime,
16
+ formatRelativeText,
17
+ formatRemainingText,
18
+ } from "@/packages/datetime/dateTime.shared";
19
+ import {
20
+ formatClientDate,
21
+ formatClientTime,
22
+ formatClientDateTime,
23
+ formatClientRelative,
24
+ } from "@/packages/datetime/dateTime.client";
25
+ import {
26
+ formatServerDate,
27
+ formatServerTime,
28
+ formatServerDateTime,
29
+ formatServerRelative,
30
+ } from "@/packages/datetime/dateTime.server";
31
+
32
+ describe("datetime module", () => {
33
+ describe("shared", () => {
34
+ describe("normalizeAppLocale", () => {
35
+ test("normalizes kr/ko", () => {
36
+ expect(normalizeAppLocale("kr")).toBe("kr");
37
+ expect(normalizeAppLocale("ko")).toBe("kr");
38
+ });
39
+ test("normalizes jp/ja", () => {
40
+ expect(normalizeAppLocale("jp")).toBe("jp");
41
+ expect(normalizeAppLocale("ja")).toBe("jp");
42
+ });
43
+ test("defaults to en", () => {
44
+ expect(normalizeAppLocale("fr")).toBe("en");
45
+ expect(normalizeAppLocale(undefined)).toBe("en");
46
+ });
47
+ });
48
+
49
+ describe("toIntlLocale", () => {
50
+ test("maps to correct intl locales", () => {
51
+ expect(toIntlLocale("kr")).toBe("ko-KR");
52
+ expect(toIntlLocale("jp")).toBe("ja-JP");
53
+ expect(toIntlLocale("en")).toBe("en-US");
54
+ });
55
+ });
56
+
57
+ describe("toDate", () => {
58
+ test("returns Date object for valid inputs", () => {
59
+ expect(toDate(new Date("2020-01-01"))).toBeInstanceOf(Date);
60
+ expect(toDate("2020-01-01")).toBeInstanceOf(Date);
61
+ expect(toDate(1577836800000)).toBeInstanceOf(Date);
62
+ });
63
+ test("returns null for invalid inputs", () => {
64
+ expect(toDate("invalid")).toBeNull();
65
+ expect(toDate(new Date("invalid"))).toBeNull();
66
+ });
67
+ });
68
+
69
+ describe("toUtcMidnight", () => {
70
+ test("converts to UTC midnight", () => {
71
+ const date = new Date("2020-01-01T15:30:00Z");
72
+ const utcDate = toUtcMidnight(date);
73
+ expect(utcDate.toISOString()).toBe("2020-01-01T00:00:00.000Z");
74
+ });
75
+ });
76
+
77
+ describe("parseUtcDateInput", () => {
78
+ test("parses YYYY-MM-DD string to UTC midnight", () => {
79
+ const result = parseUtcDateInput("2020-05-10");
80
+ expect(result?.toISOString()).toBe("2020-05-10T00:00:00.000Z");
81
+ });
82
+ test("parses Date object to UTC midnight", () => {
83
+ const result = parseUtcDateInput(new Date("2020-05-10T15:00:00Z"));
84
+ expect(result?.toISOString()).toBe("2020-05-10T00:00:00.000Z");
85
+ });
86
+ test("uses fallback", () => {
87
+ const result = parseUtcDateInput("invalid", "2020-01-01T00:00:00Z");
88
+ expect(result?.toISOString()).toBe("2020-01-01T00:00:00.000Z");
89
+ });
90
+ test("returns null if both invalid", () => {
91
+ expect(parseUtcDateInput("invalid", "invalid")).toBeNull();
92
+ });
93
+ });
94
+
95
+ describe("formatUtcDateKey", () => {
96
+ test("formats date to YYYY-MM-DD", () => {
97
+ expect(formatUtcDateKey(new Date("2020-05-05T00:00:00Z"))).toBe(
98
+ "2020-05-05",
99
+ );
100
+ });
101
+ });
102
+
103
+ describe("getUtcWeekdayIndex", () => {
104
+ test("returns correct weekday index", () => {
105
+ expect(getUtcWeekdayIndex(new Date("2020-01-05T00:00:00Z"))).toBe(0); // Sunday
106
+ expect(getUtcWeekdayIndex(new Date("2020-01-06T00:00:00Z"))).toBe(1); // Monday
107
+ });
108
+ });
109
+
110
+ describe("addUtcDays", () => {
111
+ test("adds days to date", () => {
112
+ const result = addUtcDays(new Date("2020-01-01T00:00:00Z"), 5);
113
+ expect(result.toISOString()).toBe("2020-01-06T00:00:00.000Z");
114
+ });
115
+ test("handles negative days", () => {
116
+ const result = addUtcDays(new Date("2020-01-05T00:00:00Z"), -2);
117
+ expect(result.toISOString()).toBe("2020-01-03T00:00:00.000Z");
118
+ });
119
+ });
120
+
121
+ describe("formatters", () => {
122
+ const testDate = new Date("2020-05-10T15:30:45Z"); // UTC time
123
+
124
+ test("formatLongDate", () => {
125
+ expect(formatLongDate(testDate, "kr", "UTC")).toBe("2020년 5월 10일");
126
+ expect(formatLongDate(testDate, "jp", "UTC")).toBe("2020年5月10日");
127
+ expect(formatLongDate(testDate, "en", "UTC")).toContain("2020");
128
+ });
129
+
130
+ test("formatDotDate", () => {
131
+ expect(formatDotDate(testDate, "UTC")).toBe("2020. 5. 10.");
132
+ });
133
+
134
+ test("format24HourTime", () => {
135
+ expect(format24HourTime(testDate, { timeZone: "UTC" })).toBe("15:30");
136
+ expect(
137
+ format24HourTime(testDate, { includeSeconds: true, timeZone: "UTC" }),
138
+ ).toBe("15:30:45");
139
+ });
140
+
141
+ test("formatKoreanTime", () => {
142
+ expect(formatKoreanTime(testDate, "UTC")).toBe("오후 3시 30분");
143
+ expect(formatKoreanTime(new Date("2020-05-10T09:15:00Z"), "UTC")).toBe(
144
+ "오전 9시 15분",
145
+ );
146
+ });
147
+
148
+ test("formatTwelveHourTime", () => {
149
+ expect(formatTwelveHourTime(testDate, "kr", "UTC")).toBe("오후 3:30");
150
+ expect(formatTwelveHourTime(testDate, "en", "UTC")).toMatch(
151
+ /3:30 (PM|pm)/,
152
+ );
153
+ });
154
+ });
155
+
156
+ describe("formatRelativeText", () => {
157
+ test("formats seconds", () => {
158
+ expect(formatRelativeText(30000, "en")).toEqual({
159
+ text: "30 seconds ago",
160
+ isRelative: true,
161
+ });
162
+ expect(formatRelativeText(30000, "kr")).toEqual({
163
+ text: "30초 전",
164
+ isRelative: true,
165
+ });
166
+ });
167
+ test("formats minutes", () => {
168
+ expect(formatRelativeText(120000, "en")).toEqual({
169
+ text: "2 minutes ago",
170
+ isRelative: true,
171
+ });
172
+ });
173
+ test("formats hours", () => {
174
+ expect(formatRelativeText(7200000, "en")).toEqual({
175
+ text: "2 hours ago",
176
+ isRelative: true,
177
+ });
178
+ });
179
+ test("formats days", () => {
180
+ expect(formatRelativeText(172800000, "en")).toEqual({
181
+ text: "2 days ago",
182
+ isRelative: true,
183
+ });
184
+ });
185
+ });
186
+
187
+ describe("formatRemainingText", () => {
188
+ test("formats remaining time", () => {
189
+ expect(formatRemainingText(0)).toBe("Closed");
190
+ expect(formatRemainingText(0, "kr")).toBe("마감됨");
191
+ expect(formatRemainingText(0, "jp")).toBe("締切終了");
192
+
193
+ expect(formatRemainingText(1000, "en")).toBe("1 second left");
194
+ expect(formatRemainingText(65000, "en")).toBe(
195
+ "1 minute 5 seconds left",
196
+ );
197
+ expect(formatRemainingText(3665000, "kr")).toBe("1시간 1분 남음");
198
+ expect(formatRemainingText(90065000, "jp")).toBe("あと1日1時間");
199
+ });
200
+ test("without suffix", () => {
201
+ expect(formatRemainingText(1000, "en", { includeSuffix: false })).toBe(
202
+ "1 second",
203
+ );
204
+ });
205
+ });
206
+ });
207
+
208
+ describe("client/server wrappers", () => {
209
+ const testDate = new Date("2020-05-10T15:30:45Z");
210
+
211
+ test("formatClientDateTime / formatServerDateTime", () => {
212
+ expect(formatClientDateTime(testDate, { timeZone: "UTC" })).toBe(
213
+ "May 10, 2020 15:30",
214
+ );
215
+ expect(
216
+ formatClientDateTime(testDate, { locale: "kr", timeZone: "UTC" }),
217
+ ).toBe("2020년 5월 10일 15:30");
218
+ expect(
219
+ formatServerDateTime(testDate, { locale: "kr", timeZone: "UTC" }),
220
+ ).toBe("2020년 5월 10일 15:30");
221
+ });
222
+
223
+ test("formatClientRelative / formatServerRelative", () => {
224
+ const now = new Date("2020-05-10T15:35:45Z"); // 5 mins later
225
+ expect(formatClientRelative(testDate, { now, locale: "kr" })).toEqual({
226
+ text: "5분 전",
227
+ isRelative: true,
228
+ });
229
+ expect(formatServerRelative(testDate, { now, locale: "kr" })).toEqual({
230
+ text: "5분 전",
231
+ isRelative: true,
232
+ });
233
+
234
+ const farFutureNow = new Date("2020-05-20T15:30:45Z"); // 10 days later
235
+ expect(
236
+ formatClientRelative(testDate, {
237
+ now: farFutureNow,
238
+ locale: "kr",
239
+ fallbackDatePreset: "dot",
240
+ maxRelativeDays: 7,
241
+ }),
242
+ ).toEqual({ text: formatDotDate(testDate), isRelative: false });
243
+ });
244
+
245
+ test("handles invalid inputs gracefully", () => {
246
+ expect(formatClientDate("invalid", { preset: "long" })).toBe("");
247
+ expect(formatServerTime("invalid", { preset: "ko" })).toBe("");
248
+ });
249
+ });
250
+ });
@@ -0,0 +1,12 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useAvoidKeyboard } from "@/packages/hooks/useAvoidKeyboard";
4
+
5
+ describe("useAvoidKeyboard", () => {
6
+ test("returns style object", () => {
7
+ const { result } = renderHook(() => useAvoidKeyboard());
8
+ expect(result.current.style).toBeDefined();
9
+ expect(result.current.style.transform).toBeUndefined();
10
+ expect(result.current.style.transition).toContain("transform");
11
+ });
12
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useCheckInvisible } from "@/packages/hooks/useCheckInvisible";
4
+
5
+ describe("useCheckInvisible", () => {
6
+ test("returns isInvisible false when element is visible", () => {
7
+ const el = document.createElement("div");
8
+ el.className = "test-section";
9
+ el.getBoundingClientRect = vi.fn(() => ({ top: 100, bottom: 300, left: 0, right: 100, width: 100, height: 200, x: 0, y: 100 }) as DOMRect);
10
+ document.body.appendChild(el);
11
+
12
+ const { result } = renderHook(() => useCheckInvisible("test-section"));
13
+ expect(result.current.isInvisible).toBe(false);
14
+ document.body.removeChild(el);
15
+ });
16
+
17
+ test("returns isInvisible true when element is scrolled above viewport", () => {
18
+ const el = document.createElement("div");
19
+ el.className = "test-section";
20
+ el.getBoundingClientRect = vi.fn(() => ({ top: -50, bottom: 150, left: 0, right: 100, width: 100, height: 200, x: 0, y: -50 }) as DOMRect);
21
+ document.body.appendChild(el);
22
+
23
+ const { result } = renderHook(() => useCheckInvisible("test-section"));
24
+ act(() => { window.dispatchEvent(new Event("scroll")); });
25
+ expect(result.current.isInvisible).toBe(true);
26
+ document.body.removeChild(el);
27
+ });
28
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useCheckScroll } from "@/packages/hooks/useCheckScroll";
4
+
5
+ describe("useCheckScroll", () => {
6
+ test("initializes with scrollY state", () => {
7
+ Object.defineProperty(window, "scrollY", { value: 100, writable: true });
8
+ const { result } = renderHook(() => useCheckScroll());
9
+ expect(result.current.isScrolled).toBe(true);
10
+ });
11
+
12
+ test("updates state on scroll", () => {
13
+ Object.defineProperty(window, "scrollY", { value: 0, writable: true });
14
+ const { result } = renderHook(() => useCheckScroll());
15
+ expect(result.current.isScrolled).toBe(false);
16
+
17
+ act(() => {
18
+ window.scrollY = 150;
19
+ window.dispatchEvent(new Event("scroll"));
20
+ });
21
+ expect(result.current.isScrolled).toBe(true);
22
+ });
23
+ });
@@ -0,0 +1,14 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useClientDateTime } from "@/packages/hooks/useClientDateTime";
4
+
5
+ describe("useClientDateTime", () => {
6
+ test("returns ready state and formatted text", () => {
7
+ const { result } = renderHook(() =>
8
+ useClientDateTime("2024-06-15", { locale: "ko" }),
9
+ );
10
+ expect(result.current.ready).toBe(true);
11
+ expect(result.current.text).toBeTruthy();
12
+ expect(result.current.date).toBeInstanceOf(Date);
13
+ });
14
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { debounce, useDebounce } from "@/packages/hooks/useDebounce";
4
+
5
+ describe("debounce (standalone)", () => {
6
+ beforeEach(() => { vi.useFakeTimers(); });
7
+ afterEach(() => { vi.useRealTimers(); });
8
+
9
+ test("trailing: calls function after delay", () => {
10
+ const fn = vi.fn();
11
+ const d = debounce(fn, 100, { edges: ["trailing"] });
12
+ d();
13
+ expect(fn).not.toHaveBeenCalled();
14
+ vi.advanceTimersByTime(100);
15
+ expect(fn).toHaveBeenCalledOnce();
16
+ });
17
+
18
+ test("leading + trailing: calls immediately and debounces trailing", () => {
19
+ const fn = vi.fn();
20
+ const d = debounce(fn, 100, { edges: ["leading", "trailing"] });
21
+ d();
22
+ expect(fn).toHaveBeenCalledTimes(1);
23
+ d();
24
+ vi.advanceTimersByTime(100);
25
+ expect(fn).toHaveBeenCalledTimes(2);
26
+ });
27
+
28
+ test("leading only: calls immediately, no trailing", () => {
29
+ const fn = vi.fn();
30
+ const d = debounce(fn, 100, { edges: ["leading"] });
31
+ d();
32
+ expect(fn).toHaveBeenCalledTimes(1);
33
+ vi.advanceTimersByTime(100);
34
+ expect(fn).toHaveBeenCalledTimes(1);
35
+ });
36
+
37
+ test("cancel aborts pending call", () => {
38
+ const fn = vi.fn();
39
+ const d = debounce(fn, 100, { edges: ["trailing"] });
40
+ d();
41
+ d.cancel();
42
+ vi.advanceTimersByTime(100);
43
+ expect(fn).not.toHaveBeenCalled();
44
+ });
45
+
46
+ test("multiple calls reset the timer in trailing mode", () => {
47
+ const fn = vi.fn();
48
+ const d = debounce(fn, 100, { edges: ["trailing"] });
49
+ d();
50
+ vi.advanceTimersByTime(50);
51
+ d();
52
+ vi.advanceTimersByTime(50);
53
+ expect(fn).not.toHaveBeenCalled();
54
+ vi.advanceTimersByTime(50);
55
+ expect(fn).toHaveBeenCalledOnce();
56
+ });
57
+ });
58
+
59
+ describe("useDebounce", () => {
60
+ beforeEach(() => { vi.useFakeTimers(); });
61
+ afterEach(() => { vi.useRealTimers(); });
62
+
63
+ test("returns a debounced function", () => {
64
+ const fn = vi.fn();
65
+ const { result } = renderHook(() => useDebounce(fn, 100));
66
+ result.current();
67
+ expect(fn).not.toHaveBeenCalled();
68
+ vi.advanceTimersByTime(100);
69
+ expect(fn).toHaveBeenCalledOnce();
70
+ });
71
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useDebouncedCallback } from "@/packages/hooks/useDebouncedCallback";
4
+
5
+ describe("useDebouncedCallback", () => {
6
+ beforeEach(() => { vi.useFakeTimers(); });
7
+ afterEach(() => { vi.useRealTimers(); });
8
+
9
+ test("debounces onChange calls", () => {
10
+ const onChange = vi.fn();
11
+ const { result } = renderHook(() =>
12
+ useDebouncedCallback({ onChange, timeThreshold: 100 }),
13
+ );
14
+
15
+ act(() => { result.current(true); });
16
+ vi.advanceTimersByTime(100);
17
+ expect(onChange).toHaveBeenCalledWith(true);
18
+ });
19
+
20
+ test("skips duplicate value", () => {
21
+ const onChange = vi.fn();
22
+ const { result } = renderHook(() =>
23
+ useDebouncedCallback({ onChange, timeThreshold: 100 }),
24
+ );
25
+
26
+ act(() => { result.current(true); });
27
+ vi.advanceTimersByTime(100);
28
+ expect(onChange).toHaveBeenCalledTimes(1);
29
+
30
+ act(() => { result.current(true); });
31
+ vi.advanceTimersByTime(100);
32
+ expect(onChange).toHaveBeenCalledTimes(1);
33
+ });
34
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, test, expect, vi, afterEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useDoubleClick } from "@/packages/hooks/useDoubleClick";
4
+
5
+ describe("useDoubleClick", () => {
6
+ afterEach(() => { vi.useRealTimers(); });
7
+
8
+ test("fires click on single click (event.detail === 1)", () => {
9
+ vi.useFakeTimers();
10
+ const click = vi.fn();
11
+ const doubleClick = vi.fn();
12
+ const { result } = renderHook(() => useDoubleClick({ click, doubleClick }));
13
+
14
+ const event = new MouseEvent("click", { detail: 1 }) as unknown as React.MouseEvent<HTMLElement>;
15
+ act(() => { result.current(event); });
16
+ expect(doubleClick).not.toHaveBeenCalled();
17
+ vi.advanceTimersByTime(300);
18
+ expect(click).toHaveBeenCalledOnce();
19
+ });
20
+
21
+ test("fires doubleClick on double click (event.detail === 2)", () => {
22
+ const click = vi.fn();
23
+ const doubleClick = vi.fn();
24
+ const { result } = renderHook(() => useDoubleClick({ click, doubleClick }));
25
+
26
+ const event = new MouseEvent("click", { detail: 2 }) as unknown as React.MouseEvent<HTMLElement>;
27
+ act(() => { result.current(event); });
28
+ expect(doubleClick).toHaveBeenCalledOnce();
29
+ expect(click).not.toHaveBeenCalled();
30
+ });
31
+ });
@@ -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
+ });