@musecat/functionkit 1.0.2 → 1.1.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.
Files changed (72) hide show
  1. package/.agents/references/hooks/useIntersectionObserver.md +4 -0
  2. package/.agents/references/hooks/useInterval.md +4 -0
  3. package/.agents/references/hooks/usePreservedCallback.md +4 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  7. package/.github/workflows/ci.yml +20 -0
  8. package/.local/state/gh/device-id +1 -0
  9. package/.prettierrc +8 -0
  10. package/AGENTS.md +14 -0
  11. package/CODE_OF_CONDUCT.md +55 -0
  12. package/CONTRIBUTING.md +39 -0
  13. package/biome.json +25 -0
  14. package/index.ts +1 -1
  15. package/package.json +21 -2
  16. package/packages/components/ScrolltoTop.tsx +1 -1
  17. package/packages/components/ViewportPortal.tsx +1 -2
  18. package/packages/cookie/cookie.shared.ts +7 -33
  19. package/packages/cookie/cookieNames.shared.ts +9 -0
  20. package/packages/datetime/dateTime.client.ts +1 -3
  21. package/packages/datetime/dateTime.server.ts +1 -3
  22. package/packages/datetime/dateTime.shared.ts +9 -47
  23. package/packages/hooks/useCheckInvisible.ts +1 -1
  24. package/packages/hooks/useClientDateTime.ts +3 -16
  25. package/packages/hooks/useDebounce.ts +2 -7
  26. package/packages/hooks/useGeolocation.ts +1 -5
  27. package/packages/hooks/useInterval.ts +1 -2
  28. package/packages/hooks/useKeyboardListNavigation.ts +10 -38
  29. package/packages/hooks/useLongPress.ts +13 -24
  30. package/packages/hooks/useRelativeDateTime.ts +1 -4
  31. package/packages/hooks/useViewportHeight.ts +1 -2
  32. package/packages/hooks/useViewportMatch.ts +0 -4
  33. package/packages/utils/buildContext.tsx +2 -3
  34. package/packages/utils/floatingMotion.ts +14 -20
  35. package/packages/utils/subscribeKeyboardHeight.ts +3 -0
  36. package/tests/components/ScrolltoTop.test.tsx +1 -1
  37. package/tests/components/SwitchCase.test.tsx +1 -2
  38. package/tests/components/ViewportPortal.test.tsx +10 -6
  39. package/tests/cookie/cookie.test.ts +47 -11
  40. package/tests/datetime/datetime.test.ts +257 -46
  41. package/tests/hooks/useAvoidKeyboard.test.ts +32 -1
  42. package/tests/hooks/useCheckInvisible.test.ts +36 -5
  43. package/tests/hooks/useCheckScroll.test.ts +2 -2
  44. package/tests/hooks/useClientDateTime.test.ts +2 -4
  45. package/tests/hooks/useDebounce.test.ts +30 -5
  46. package/tests/hooks/useDebouncedCallback.test.ts +41 -12
  47. package/tests/hooks/useDoubleClick.test.ts +58 -7
  48. package/tests/hooks/useGeolocation.test.ts +155 -4
  49. package/tests/hooks/useHasMounted.test.ts +1 -1
  50. package/tests/hooks/useIntersectionObserver.test.ts +70 -5
  51. package/tests/hooks/useInterval.test.ts +28 -3
  52. package/tests/hooks/useKeyboardHeight.test.ts +1 -1
  53. package/tests/hooks/useKeyboardListNavigation.test.ts +340 -18
  54. package/tests/hooks/useLongPress.test.ts +177 -15
  55. package/tests/hooks/usePreservedCallback.test.ts +7 -9
  56. package/tests/hooks/usePreservedReference.test.ts +19 -12
  57. package/tests/hooks/useRefEffect.test.ts +43 -6
  58. package/tests/hooks/useRelativeDateTime.test.ts +8 -6
  59. package/tests/hooks/useTimeout.test.ts +43 -5
  60. package/tests/hooks/useToggleState.test.ts +14 -6
  61. package/tests/hooks/useViewportHeight.test.ts +24 -2
  62. package/tests/hooks/useViewportMatch.test.ts +1 -1
  63. package/tests/utils/browserStorage.test.ts +68 -8
  64. package/tests/utils/buildContext.test.tsx +1 -2
  65. package/tests/utils/checkDevice.test.ts +62 -6
  66. package/tests/utils/clipboardShare.test.ts +79 -5
  67. package/tests/utils/floatingMotion.test.ts +60 -9
  68. package/tests/utils/keyboardTarget.test.ts +42 -1
  69. package/tests/utils/mergeRefs.test.ts +33 -1
  70. package/tests/utils/seen.test.ts +16 -7
  71. package/tests/utils/subscribeKeyboardHeight.test.ts +124 -2
  72. package/vitest.config.ts +7 -1
@@ -1,33 +1,33 @@
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";
1
+ import { describe, expect, test, vi } from "vitest";
19
2
  import {
20
3
  formatClientDate,
21
- formatClientTime,
22
4
  formatClientDateTime,
23
5
  formatClientRelative,
6
+ formatClientTime,
24
7
  } from "@/packages/datetime/dateTime.client";
25
8
  import {
26
9
  formatServerDate,
27
- formatServerTime,
28
10
  formatServerDateTime,
29
11
  formatServerRelative,
12
+ formatServerTime,
30
13
  } from "@/packages/datetime/dateTime.server";
14
+ import {
15
+ addUtcDays,
16
+ format24HourTime,
17
+ formatDotDate,
18
+ formatKoreanTime,
19
+ formatLongDate,
20
+ formatRelativeText,
21
+ formatRemainingText,
22
+ formatTwelveHourTime,
23
+ formatUtcDateKey,
24
+ getUtcWeekdayIndex,
25
+ normalizeAppLocale,
26
+ parseUtcDateInput,
27
+ toDate,
28
+ toIntlLocale,
29
+ toUtcMidnight,
30
+ } from "@/packages/datetime/dateTime.shared";
31
31
 
32
32
  describe("datetime module", () => {
33
33
  describe("shared", () => {
@@ -90,13 +90,23 @@ describe("datetime module", () => {
90
90
  test("returns null if both invalid", () => {
91
91
  expect(parseUtcDateInput("invalid", "invalid")).toBeNull();
92
92
  });
93
+
94
+ test("returns null when no fallback provided", () => {
95
+ expect(parseUtcDateInput("invalid")).toBeNull();
96
+ });
97
+
98
+ test("handles undefined value", () => {
99
+ expect(parseUtcDateInput(undefined)).toBeNull();
100
+ });
101
+
102
+ test("handles null value", () => {
103
+ expect(parseUtcDateInput(null)).toBeNull();
104
+ });
93
105
  });
94
106
 
95
107
  describe("formatUtcDateKey", () => {
96
108
  test("formats date to YYYY-MM-DD", () => {
97
- expect(formatUtcDateKey(new Date("2020-05-05T00:00:00Z"))).toBe(
98
- "2020-05-05",
99
- );
109
+ expect(formatUtcDateKey(new Date("2020-05-05T00:00:00Z"))).toBe("2020-05-05");
100
110
  });
101
111
  });
102
112
 
@@ -127,33 +137,63 @@ describe("datetime module", () => {
127
137
  expect(formatLongDate(testDate, "en", "UTC")).toContain("2020");
128
138
  });
129
139
 
140
+ test("handles missing date/time parts gracefully", () => {
141
+ const spy = vi.spyOn(Intl.DateTimeFormat.prototype, "formatToParts").mockReturnValue([]);
142
+ try {
143
+ expect(formatLongDate(testDate, "kr", "UTC")).toBe("0000년 0월 0일");
144
+ expect(format24HourTime(testDate)).toBe("00:00");
145
+ } finally {
146
+ spy.mockRestore();
147
+ }
148
+ });
149
+
130
150
  test("formatDotDate", () => {
131
151
  expect(formatDotDate(testDate, "UTC")).toBe("2020. 5. 10.");
132
152
  });
133
153
 
134
154
  test("format24HourTime", () => {
135
155
  expect(format24HourTime(testDate, { timeZone: "UTC" })).toBe("15:30");
136
- expect(
137
- format24HourTime(testDate, { includeSeconds: true, timeZone: "UTC" }),
138
- ).toBe("15:30:45");
156
+ expect(format24HourTime(testDate, { includeSeconds: true, timeZone: "UTC" })).toBe(
157
+ "15:30:45",
158
+ );
159
+ expect(format24HourTime(testDate)).toBe("15:30");
139
160
  });
140
161
 
141
162
  test("formatKoreanTime", () => {
142
163
  expect(formatKoreanTime(testDate, "UTC")).toBe("오후 3시 30분");
143
- expect(formatKoreanTime(new Date("2020-05-10T09:15:00Z"), "UTC")).toBe(
144
- "오전 915분",
145
- );
164
+ expect(formatKoreanTime(new Date("2020-05-10T09:15:00Z"), "UTC")).toBe("오전 9시 15분");
165
+ expect(formatKoreanTime(new Date("2020-05-10T00:30:00Z"), "UTC")).toBe("오전 1230분");
166
+ expect(formatKoreanTime(new Date("2020-05-10T12:30:00Z"), "UTC")).toBe("오후 12시 30분");
146
167
  });
147
168
 
148
169
  test("formatTwelveHourTime", () => {
149
170
  expect(formatTwelveHourTime(testDate, "kr", "UTC")).toBe("오후 3:30");
150
- expect(formatTwelveHourTime(testDate, "en", "UTC")).toMatch(
151
- /3:30 (PM|pm)/,
152
- );
171
+ expect(formatTwelveHourTime(new Date("2020-05-10T00:30:00Z"), "kr", "UTC")).toBe("오전 12:30");
172
+ expect(formatTwelveHourTime(new Date("2020-05-10T12:30:00Z"), "kr", "UTC")).toBe("오후 12:30");
173
+ expect(formatTwelveHourTime(testDate, "en", "UTC")).toMatch(/3:30 (PM|pm)/);
153
174
  });
154
175
  });
155
176
 
156
177
  describe("formatRelativeText", () => {
178
+ test("formatRelativeUnit with singular count (1 second, 1 minute, etc)", () => {
179
+ expect(formatRelativeText(1000, "en")).toEqual({
180
+ text: "1 second ago",
181
+ isRelative: true,
182
+ });
183
+ expect(formatRelativeText(60000, "en")).toEqual({
184
+ text: "1 minute ago",
185
+ isRelative: true,
186
+ });
187
+ expect(formatRelativeText(3600000, "en")).toEqual({
188
+ text: "1 hour ago",
189
+ isRelative: true,
190
+ });
191
+ expect(formatRelativeText(86400000, "en")).toEqual({
192
+ text: "1 day ago",
193
+ isRelative: true,
194
+ });
195
+ });
196
+
157
197
  test("formats seconds", () => {
158
198
  expect(formatRelativeText(30000, "en")).toEqual({
159
199
  text: "30 seconds ago",
@@ -163,24 +203,52 @@ describe("datetime module", () => {
163
203
  text: "30초 전",
164
204
  isRelative: true,
165
205
  });
206
+ expect(formatRelativeText(30000, "jp")).toEqual({
207
+ text: "30秒前",
208
+ isRelative: true,
209
+ });
166
210
  });
167
211
  test("formats minutes", () => {
168
212
  expect(formatRelativeText(120000, "en")).toEqual({
169
213
  text: "2 minutes ago",
170
214
  isRelative: true,
171
215
  });
216
+ expect(formatRelativeText(120000, "kr")).toEqual({
217
+ text: "2분 전",
218
+ isRelative: true,
219
+ });
220
+ expect(formatRelativeText(120000, "jp")).toEqual({
221
+ text: "2分前",
222
+ isRelative: true,
223
+ });
172
224
  });
173
225
  test("formats hours", () => {
174
226
  expect(formatRelativeText(7200000, "en")).toEqual({
175
227
  text: "2 hours ago",
176
228
  isRelative: true,
177
229
  });
230
+ expect(formatRelativeText(7200000, "kr")).toEqual({
231
+ text: "2시간 전",
232
+ isRelative: true,
233
+ });
234
+ expect(formatRelativeText(7200000, "jp")).toEqual({
235
+ text: "2時間前",
236
+ isRelative: true,
237
+ });
178
238
  });
179
239
  test("formats days", () => {
180
240
  expect(formatRelativeText(172800000, "en")).toEqual({
181
241
  text: "2 days ago",
182
242
  isRelative: true,
183
243
  });
244
+ expect(formatRelativeText(172800000, "kr")).toEqual({
245
+ text: "2일 전",
246
+ isRelative: true,
247
+ });
248
+ expect(formatRelativeText(172800000, "jp")).toEqual({
249
+ text: "2日前",
250
+ isRelative: true,
251
+ });
184
252
  });
185
253
  });
186
254
 
@@ -191,16 +259,64 @@ describe("datetime module", () => {
191
259
  expect(formatRemainingText(0, "jp")).toBe("締切終了");
192
260
 
193
261
  expect(formatRemainingText(1000, "en")).toBe("1 second left");
194
- expect(formatRemainingText(65000, "en")).toBe(
195
- "1 minute 5 seconds left",
196
- );
262
+ expect(formatRemainingText(65000, "en")).toBe("1 minute 5 seconds left");
197
263
  expect(formatRemainingText(3665000, "kr")).toBe("1시간 1분 남음");
198
264
  expect(formatRemainingText(90065000, "jp")).toBe("あと1日1時間");
199
265
  });
200
266
  test("without suffix", () => {
201
- expect(formatRemainingText(1000, "en", { includeSuffix: false })).toBe(
202
- "1 second",
203
- );
267
+ expect(formatRemainingText(1000, "en", { includeSuffix: false })).toBe("1 second");
268
+ });
269
+
270
+ test("remaining time less than 1 second defaults to 1 second", () => {
271
+ expect(formatRemainingText(500, "en")).toBe("1 second left");
272
+ });
273
+
274
+ test("formatRemainingText jp locale with no suffix", () => {
275
+ expect(formatRemainingText(3665000, "jp", { includeSuffix: false })).toBe("1時間1分");
276
+ });
277
+
278
+ test("formatRemainingUnit for kr locale covers all units", () => {
279
+ expect(formatRemainingText(1000, "kr")).toBe("1초 남음");
280
+ expect(formatRemainingText(60000, "kr")).toBe("1분 남음");
281
+ expect(formatRemainingText(3600000, "kr")).toBe("1시간 남음");
282
+ expect(formatRemainingText(86400000, "kr")).toBe("1일 남음");
283
+ });
284
+
285
+ test("formatRemainingUnit for jp locale", () => {
286
+ expect(formatRemainingText(1000, "jp")).toBe("あと1秒");
287
+ expect(formatRemainingText(60000, "jp")).toBe("あと1分");
288
+ expect(formatRemainingText(3600000, "jp")).toBe("あと1時間");
289
+ expect(formatRemainingText(86400000, "jp")).toBe("あと1日");
290
+ });
291
+
292
+ test("formatRemainingUnit for en locale covers all units", () => {
293
+ expect(formatRemainingText(1000, "en")).toBe("1 second left");
294
+ expect(formatRemainingText(60000, "en")).toBe("1 minute left");
295
+ expect(formatRemainingText(3600000, "en")).toBe("1 hour left");
296
+ expect(formatRemainingText(86400000, "en")).toBe("1 day left");
297
+ });
298
+
299
+ test("formatLongDate with undefined locale defaults to en", () => {
300
+ expect(formatLongDate(new Date("2020-05-10T15:00:00Z"), undefined)).toContain("2020");
301
+ });
302
+
303
+ test("formatRelativeText with jp locale", () => {
304
+ expect(formatRelativeText(30000, "jp")).toEqual({
305
+ text: "30秒前",
306
+ isRelative: true,
307
+ });
308
+ expect(formatRelativeText(120000, "jp")).toEqual({
309
+ text: "2分前",
310
+ isRelative: true,
311
+ });
312
+ expect(formatRelativeText(7200000, "jp")).toEqual({
313
+ text: "2時間前",
314
+ isRelative: true,
315
+ });
316
+ expect(formatRelativeText(172800000, "jp")).toEqual({
317
+ text: "2日前",
318
+ isRelative: true,
319
+ });
204
320
  });
205
321
  });
206
322
  });
@@ -209,15 +325,41 @@ describe("datetime module", () => {
209
325
  const testDate = new Date("2020-05-10T15:30:45Z");
210
326
 
211
327
  test("formatClientDateTime / formatServerDateTime", () => {
212
- expect(formatClientDateTime(testDate, { timeZone: "UTC" })).toBe(
213
- "May 10, 2020 15:30",
328
+ expect(formatClientDateTime(testDate, { timeZone: "UTC" })).toBe("May 10, 2020 15:30");
329
+ expect(formatClientDateTime(testDate, { locale: "kr", timeZone: "UTC" })).toBe(
330
+ "2020년 5월 10일 15:30",
214
331
  );
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");
332
+ expect(formatServerDateTime(testDate, { locale: "kr", timeZone: "UTC" })).toBe(
333
+ "2020년 5월 10일 15:30",
334
+ );
335
+ });
336
+
337
+ test("formatClientTime with various presets", () => {
338
+ expect(formatClientTime(testDate, { preset: "ko", timeZone: "UTC" })).toBe("오후 3시 30분");
339
+ expect(formatClientTime(testDate, { preset: "12h", locale: "en", timeZone: "UTC" })).toMatch(
340
+ /3:30 (PM|pm)/,
341
+ );
342
+ expect(formatClientTime(testDate, { preset: "24h-second", timeZone: "UTC" })).toBe(
343
+ "15:30:45",
344
+ );
345
+ });
346
+
347
+ test("formatServerTime with various presets", () => {
348
+ expect(formatServerTime(testDate, { preset: "ko", timeZone: "UTC" })).toBe("오후 3시 30분");
349
+ expect(formatServerTime(testDate, { preset: "12h", locale: "en", timeZone: "UTC" })).toMatch(
350
+ /3:30 (PM|pm)/,
351
+ );
352
+ expect(formatServerTime(testDate, { preset: "24h-second", timeZone: "UTC" })).toBe(
353
+ "15:30:45",
354
+ );
355
+ });
356
+
357
+ test("formatClientTime returns empty for invalid date", () => {
358
+ expect(formatClientTime("invalid", { preset: "24h-minute" })).toBe("");
359
+ });
360
+
361
+ test("formatClientDateTime returns only date or time when one is empty", () => {
362
+ expect(formatClientDateTime("invalid", {})).toBe("");
221
363
  });
222
364
 
223
365
  test("formatClientRelative / formatServerRelative", () => {
@@ -242,6 +384,75 @@ describe("datetime module", () => {
242
384
  ).toEqual({ text: formatDotDate(testDate), isRelative: false });
243
385
  });
244
386
 
387
+ test("formatClientRelative returns empty when given invalid date", () => {
388
+ expect(formatClientRelative("invalid", {})).toEqual({ text: "", isRelative: false });
389
+ });
390
+
391
+ test("formatClientRelative beyond maxRelative without fallbackDatePreset uses dot default", () => {
392
+ const oldDate = new Date("2020-01-01T00:00:00Z");
393
+ const now = new Date("2020-05-10T00:00:00Z");
394
+ const result = formatClientRelative(oldDate, { now, maxRelativeDays: 30, locale: "en" });
395
+ expect(result.isRelative).toBe(false);
396
+ expect(result.text).toContain("2020");
397
+ });
398
+
399
+ test("formatClientRelative returns empty when now is invalid", () => {
400
+ expect(formatClientRelative(testDate, { now: "invalid" })).toEqual({
401
+ text: "",
402
+ isRelative: false,
403
+ });
404
+ });
405
+
406
+ test("formatServerRelative returns empty when given invalid date", () => {
407
+ expect(formatServerRelative("invalid", {})).toEqual({ text: "", isRelative: false });
408
+ });
409
+
410
+ test("formatServerRelative returns empty when now is invalid", () => {
411
+ expect(formatServerRelative(testDate, { now: "invalid" })).toEqual({
412
+ text: "",
413
+ isRelative: false,
414
+ });
415
+ });
416
+
417
+ test("formatServerRelative beyond maxRelativeDays with fallback preset", () => {
418
+ const oldDate = new Date("2020-01-01T00:00:00Z");
419
+ const now = new Date("2020-05-10T00:00:00Z");
420
+ const result = formatServerRelative(oldDate, {
421
+ now,
422
+ maxRelativeDays: 30,
423
+ fallbackDatePreset: "long",
424
+ locale: "en",
425
+ });
426
+ expect(result.isRelative).toBe(false);
427
+ expect(typeof result.text).toBe("string");
428
+ });
429
+
430
+ test("formatServerRelative beyond maxRelative without fallbackDatePreset", () => {
431
+ const oldDate = new Date("2020-01-01T00:00:00Z");
432
+ const now = new Date("2020-05-10T00:00:00Z");
433
+ const result = formatServerRelative(oldDate, { now, maxRelativeDays: 30, locale: "en" });
434
+ expect(result.isRelative).toBe(false);
435
+ expect(result.text).toContain("2020");
436
+ });
437
+
438
+ test("formatServerDate returns empty for invalid date", () => {
439
+ expect(formatServerDate("invalid", { preset: "dot" })).toBe("");
440
+ });
441
+
442
+ test("formatServerDate with dot preset", () => {
443
+ expect(formatServerDate(testDate, { preset: "dot", timeZone: "UTC" })).toBe("2020. 5. 10.");
444
+ });
445
+
446
+ test("formatServerDate with long preset", () => {
447
+ expect(formatServerDate(testDate, { preset: "long", locale: "kr", timeZone: "UTC" })).toBe(
448
+ "2020년 5월 10일",
449
+ );
450
+ });
451
+
452
+ test("formatServerDateTime returns only date when time is empty", () => {
453
+ expect(formatServerDateTime("invalid", {})).toBe("");
454
+ });
455
+
245
456
  test("handles invalid inputs gracefully", () => {
246
457
  expect(formatClientDate("invalid", { preset: "long" })).toBe("");
247
458
  expect(formatServerTime("invalid", { preset: "ko" })).toBe("");
@@ -1,12 +1,43 @@
1
- import { describe, test, expect } from "vitest";
2
1
  import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, test } from "vitest";
3
3
  import { useAvoidKeyboard } from "@/packages/hooks/useAvoidKeyboard";
4
4
 
5
+ import { useKeyboardHeight } from "@/packages/hooks/useKeyboardHeight";
6
+
7
+ vi.mock("@/packages/hooks/useKeyboardHeight");
8
+
5
9
  describe("useAvoidKeyboard", () => {
10
+ beforeEach(() => {
11
+ vi.mocked(useKeyboardHeight).mockReturnValue({ keyboardHeight: 0 });
12
+ });
13
+
6
14
  test("returns style object", () => {
7
15
  const { result } = renderHook(() => useAvoidKeyboard());
8
16
  expect(result.current.style).toBeDefined();
9
17
  expect(result.current.style.transform).toBeUndefined();
10
18
  expect(result.current.style.transition).toContain("transform");
11
19
  });
20
+
21
+ test("applies translateY when keyboard is raised", () => {
22
+ vi.mocked(useKeyboardHeight).mockReturnValue({ keyboardHeight: 300 });
23
+ const { result } = renderHook(() => useAvoidKeyboard());
24
+ expect(result.current.style.transform).toBe("translateY(-300px)");
25
+ });
26
+
27
+ test("uses safeAreaBottom when keyboardHeight is 0", () => {
28
+ const { result } = renderHook(() => useAvoidKeyboard({ safeAreaBottom: 20 }));
29
+ expect(result.current.style.transform).toBe("translateY(-20px)");
30
+ });
31
+
32
+ test("no transform when both keyboardHeight and safeAreaBottom are 0", () => {
33
+ const { result } = renderHook(() => useAvoidKeyboard());
34
+ expect(result.current.style.transform).toBeUndefined();
35
+ });
36
+
37
+ test("accepts custom transition values", () => {
38
+ const { result } = renderHook(() =>
39
+ useAvoidKeyboard({ transitionDuration: 300, transitionTimingFunction: "linear" }),
40
+ );
41
+ expect(result.current.style.transition).toBe("transform 300ms linear");
42
+ });
12
43
  });
@@ -1,12 +1,24 @@
1
- import { describe, test, expect, vi } from "vitest";
2
- import { renderHook, act } from "@testing-library/react";
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { describe, expect, test, vi } from "vitest";
3
3
  import { useCheckInvisible } from "@/packages/hooks/useCheckInvisible";
4
4
 
5
5
  describe("useCheckInvisible", () => {
6
6
  test("returns isInvisible false when element is visible", () => {
7
7
  const el = document.createElement("div");
8
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);
9
+ el.getBoundingClientRect = vi.fn(
10
+ () =>
11
+ ({
12
+ top: 100,
13
+ bottom: 300,
14
+ left: 0,
15
+ right: 100,
16
+ width: 100,
17
+ height: 200,
18
+ x: 0,
19
+ y: 100,
20
+ }) as DOMRect,
21
+ );
10
22
  document.body.appendChild(el);
11
23
 
12
24
  const { result } = renderHook(() => useCheckInvisible("test-section"));
@@ -14,14 +26,33 @@ describe("useCheckInvisible", () => {
14
26
  document.body.removeChild(el);
15
27
  });
16
28
 
29
+ test("returns isInvisible false when element is not found in DOM", () => {
30
+ const { result } = renderHook(() => useCheckInvisible("non-existent"));
31
+ expect(result.current.isInvisible).toBe(false);
32
+ });
33
+
17
34
  test("returns isInvisible true when element is scrolled above viewport", () => {
18
35
  const el = document.createElement("div");
19
36
  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);
37
+ el.getBoundingClientRect = vi.fn(
38
+ () =>
39
+ ({
40
+ top: -50,
41
+ bottom: 150,
42
+ left: 0,
43
+ right: 100,
44
+ width: 100,
45
+ height: 200,
46
+ x: 0,
47
+ y: -50,
48
+ }) as DOMRect,
49
+ );
21
50
  document.body.appendChild(el);
22
51
 
23
52
  const { result } = renderHook(() => useCheckInvisible("test-section"));
24
- act(() => { window.dispatchEvent(new Event("scroll")); });
53
+ act(() => {
54
+ window.dispatchEvent(new Event("scroll"));
55
+ });
25
56
  expect(result.current.isInvisible).toBe(true);
26
57
  document.body.removeChild(el);
27
58
  });
@@ -1,5 +1,5 @@
1
- import { describe, test, expect } from "vitest";
2
- import { renderHook, act } from "@testing-library/react";
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { describe, expect, test } from "vitest";
3
3
  import { useCheckScroll } from "@/packages/hooks/useCheckScroll";
4
4
 
5
5
  describe("useCheckScroll", () => {
@@ -1,12 +1,10 @@
1
- import { describe, test, expect } from "vitest";
2
1
  import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, test } from "vitest";
3
3
  import { useClientDateTime } from "@/packages/hooks/useClientDateTime";
4
4
 
5
5
  describe("useClientDateTime", () => {
6
6
  test("returns ready state and formatted text", () => {
7
- const { result } = renderHook(() =>
8
- useClientDateTime("2024-06-15", { locale: "ko" }),
9
- );
7
+ const { result } = renderHook(() => useClientDateTime("2024-06-15", { locale: "ko" }));
10
8
  expect(result.current.ready).toBe(true);
11
9
  expect(result.current.text).toBeTruthy();
12
10
  expect(result.current.date).toBeInstanceOf(Date);
@@ -1,10 +1,14 @@
1
- import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
1
  import { renderHook } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
3
3
  import { debounce, useDebounce } from "@/packages/hooks/useDebounce";
4
4
 
5
5
  describe("debounce (standalone)", () => {
6
- beforeEach(() => { vi.useFakeTimers(); });
7
- afterEach(() => { vi.useRealTimers(); });
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ });
8
12
 
9
13
  test("trailing: calls function after delay", () => {
10
14
  const fn = vi.fn();
@@ -57,8 +61,12 @@ describe("debounce (standalone)", () => {
57
61
  });
58
62
 
59
63
  describe("useDebounce", () => {
60
- beforeEach(() => { vi.useFakeTimers(); });
61
- afterEach(() => { vi.useRealTimers(); });
64
+ beforeEach(() => {
65
+ vi.useFakeTimers();
66
+ });
67
+ afterEach(() => {
68
+ vi.useRealTimers();
69
+ });
62
70
 
63
71
  test("returns a debounced function", () => {
64
72
  const fn = vi.fn();
@@ -68,4 +76,21 @@ describe("useDebounce", () => {
68
76
  vi.advanceTimersByTime(100);
69
77
  expect(fn).toHaveBeenCalledOnce();
70
78
  });
79
+
80
+ test("works with leading: true", () => {
81
+ const fn = vi.fn();
82
+ const { result } = renderHook(() => useDebounce(fn, 100, { leading: true }));
83
+ result.current();
84
+ expect(fn).toHaveBeenCalledOnce();
85
+ vi.advanceTimersByTime(100);
86
+ expect(fn).toHaveBeenCalledTimes(1);
87
+ });
88
+
89
+ test("works with trailing: false", () => {
90
+ const fn = vi.fn();
91
+ const { result } = renderHook(() => useDebounce(fn, 100, { trailing: false }));
92
+ result.current();
93
+ vi.advanceTimersByTime(100);
94
+ expect(fn).not.toHaveBeenCalled();
95
+ });
71
96
  });
@@ -1,33 +1,62 @@
1
- import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { renderHook, act } from "@testing-library/react";
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
3
3
  import { useDebouncedCallback } from "@/packages/hooks/useDebouncedCallback";
4
4
 
5
5
  describe("useDebouncedCallback", () => {
6
- beforeEach(() => { vi.useFakeTimers(); });
7
- afterEach(() => { vi.useRealTimers(); });
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ });
8
12
 
9
13
  test("debounces onChange calls", () => {
10
14
  const onChange = vi.fn();
11
- const { result } = renderHook(() =>
12
- useDebouncedCallback({ onChange, timeThreshold: 100 }),
13
- );
15
+ const { result } = renderHook(() => useDebouncedCallback({ onChange, timeThreshold: 100 }));
14
16
 
15
- act(() => { result.current(true); });
17
+ act(() => {
18
+ result.current(true);
19
+ });
16
20
  vi.advanceTimersByTime(100);
17
21
  expect(onChange).toHaveBeenCalledWith(true);
18
22
  });
19
23
 
20
- test("skips duplicate value", () => {
24
+ test("works with leading: true", () => {
21
25
  const onChange = vi.fn();
22
26
  const { result } = renderHook(() =>
23
- useDebouncedCallback({ onChange, timeThreshold: 100 }),
27
+ useDebouncedCallback({ onChange, timeThreshold: 100, leading: true }),
24
28
  );
29
+ act(() => {
30
+ result.current(true);
31
+ });
32
+ expect(onChange).toHaveBeenCalledOnce();
33
+ });
34
+
35
+ test("works with trailing: false", () => {
36
+ const onChange = vi.fn();
37
+ const { result } = renderHook(() =>
38
+ useDebouncedCallback({ onChange, timeThreshold: 100, trailing: false }),
39
+ );
40
+ act(() => {
41
+ result.current(true);
42
+ });
43
+ vi.advanceTimersByTime(100);
44
+ expect(onChange).not.toHaveBeenCalled();
45
+ });
46
+
47
+ test("skips duplicate value", () => {
48
+ const onChange = vi.fn();
49
+ const { result } = renderHook(() => useDebouncedCallback({ onChange, timeThreshold: 100 }));
25
50
 
26
- act(() => { result.current(true); });
51
+ act(() => {
52
+ result.current(true);
53
+ });
27
54
  vi.advanceTimersByTime(100);
28
55
  expect(onChange).toHaveBeenCalledTimes(1);
29
56
 
30
- act(() => { result.current(true); });
57
+ act(() => {
58
+ result.current(true);
59
+ });
31
60
  vi.advanceTimersByTime(100);
32
61
  expect(onChange).toHaveBeenCalledTimes(1);
33
62
  });