@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.
- package/.agents/references/hooks/useIntersectionObserver.md +4 -0
- package/.agents/references/hooks/useInterval.md +4 -0
- package/.agents/references/hooks/usePreservedCallback.md +4 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/.github/workflows/ci.yml +20 -0
- package/.local/state/gh/device-id +1 -0
- package/.prettierrc +8 -0
- package/AGENTS.md +14 -0
- package/CODE_OF_CONDUCT.md +55 -0
- package/CONTRIBUTING.md +39 -0
- package/biome.json +25 -0
- package/index.ts +1 -1
- package/package.json +21 -2
- package/packages/components/ScrolltoTop.tsx +1 -1
- package/packages/components/ViewportPortal.tsx +1 -2
- package/packages/cookie/cookie.shared.ts +7 -33
- package/packages/cookie/cookieNames.shared.ts +9 -0
- package/packages/datetime/dateTime.client.ts +1 -3
- package/packages/datetime/dateTime.server.ts +1 -3
- package/packages/datetime/dateTime.shared.ts +9 -47
- package/packages/hooks/useCheckInvisible.ts +1 -1
- package/packages/hooks/useClientDateTime.ts +3 -16
- package/packages/hooks/useDebounce.ts +2 -7
- package/packages/hooks/useGeolocation.ts +1 -5
- package/packages/hooks/useInterval.ts +1 -2
- package/packages/hooks/useKeyboardListNavigation.ts +10 -38
- package/packages/hooks/useLongPress.ts +13 -24
- package/packages/hooks/useRelativeDateTime.ts +1 -4
- package/packages/hooks/useViewportHeight.ts +1 -2
- package/packages/hooks/useViewportMatch.ts +0 -4
- package/packages/utils/buildContext.tsx +2 -3
- package/packages/utils/floatingMotion.ts +14 -20
- package/packages/utils/subscribeKeyboardHeight.ts +3 -0
- package/tests/components/ScrolltoTop.test.tsx +1 -1
- package/tests/components/SwitchCase.test.tsx +1 -2
- package/tests/components/ViewportPortal.test.tsx +10 -6
- package/tests/cookie/cookie.test.ts +47 -11
- package/tests/datetime/datetime.test.ts +257 -46
- package/tests/hooks/useAvoidKeyboard.test.ts +32 -1
- package/tests/hooks/useCheckInvisible.test.ts +36 -5
- package/tests/hooks/useCheckScroll.test.ts +2 -2
- package/tests/hooks/useClientDateTime.test.ts +2 -4
- package/tests/hooks/useDebounce.test.ts +30 -5
- package/tests/hooks/useDebouncedCallback.test.ts +41 -12
- package/tests/hooks/useDoubleClick.test.ts +58 -7
- package/tests/hooks/useGeolocation.test.ts +155 -4
- package/tests/hooks/useHasMounted.test.ts +1 -1
- package/tests/hooks/useIntersectionObserver.test.ts +70 -5
- package/tests/hooks/useInterval.test.ts +28 -3
- package/tests/hooks/useKeyboardHeight.test.ts +1 -1
- package/tests/hooks/useKeyboardListNavigation.test.ts +340 -18
- package/tests/hooks/useLongPress.test.ts +177 -15
- package/tests/hooks/usePreservedCallback.test.ts +7 -9
- package/tests/hooks/usePreservedReference.test.ts +19 -12
- package/tests/hooks/useRefEffect.test.ts +43 -6
- package/tests/hooks/useRelativeDateTime.test.ts +8 -6
- package/tests/hooks/useTimeout.test.ts +43 -5
- package/tests/hooks/useToggleState.test.ts +14 -6
- package/tests/hooks/useViewportHeight.test.ts +24 -2
- package/tests/hooks/useViewportMatch.test.ts +1 -1
- package/tests/utils/browserStorage.test.ts +68 -8
- package/tests/utils/buildContext.test.tsx +1 -2
- package/tests/utils/checkDevice.test.ts +62 -6
- package/tests/utils/clipboardShare.test.ts +79 -5
- package/tests/utils/floatingMotion.test.ts +60 -9
- package/tests/utils/keyboardTarget.test.ts +42 -1
- package/tests/utils/mergeRefs.test.ts +33 -1
- package/tests/utils/seen.test.ts +16 -7
- package/tests/utils/subscribeKeyboardHeight.test.ts +124 -2
- package/vitest.config.ts +7 -1
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { describe, test,
|
|
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
|
-
|
|
138
|
-
)
|
|
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
|
-
|
|
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("오전 12시 30분");
|
|
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(
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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(
|
|
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(
|
|
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(() => {
|
|
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 {
|
|
2
|
-
import {
|
|
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(() => {
|
|
7
|
-
|
|
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(() => {
|
|
61
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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(() => {
|
|
7
|
-
|
|
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(() => {
|
|
17
|
+
act(() => {
|
|
18
|
+
result.current(true);
|
|
19
|
+
});
|
|
16
20
|
vi.advanceTimersByTime(100);
|
|
17
21
|
expect(onChange).toHaveBeenCalledWith(true);
|
|
18
22
|
});
|
|
19
23
|
|
|
20
|
-
test("
|
|
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(() => {
|
|
51
|
+
act(() => {
|
|
52
|
+
result.current(true);
|
|
53
|
+
});
|
|
27
54
|
vi.advanceTimersByTime(100);
|
|
28
55
|
expect(onChange).toHaveBeenCalledTimes(1);
|
|
29
56
|
|
|
30
|
-
act(() => {
|
|
57
|
+
act(() => {
|
|
58
|
+
result.current(true);
|
|
59
|
+
});
|
|
31
60
|
vi.advanceTimersByTime(100);
|
|
32
61
|
expect(onChange).toHaveBeenCalledTimes(1);
|
|
33
62
|
});
|