@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.
- package/.agents/references/components/ScrolltoTop.md +36 -0
- package/.agents/references/components/SwitchCase.md +47 -0
- package/.agents/references/components/ViewportPortal.md +34 -0
- package/.agents/references/cookie/cookie.md +47 -0
- package/.agents/references/datetime/dateTime.client.md +42 -0
- package/.agents/references/datetime/dateTime.server.md +42 -0
- package/.agents/references/datetime/dateTime.shared.md +86 -0
- package/.agents/references/hooks/useAvoidKeyboard.md +29 -0
- package/.agents/references/hooks/useCheckInvisible.md +36 -0
- package/.agents/references/hooks/useCheckScroll.md +36 -0
- package/.agents/references/hooks/useClientDateTime.md +39 -0
- package/.agents/references/hooks/useDebounce.md +46 -0
- package/.agents/references/hooks/useDebouncedCallback.md +34 -0
- package/.agents/references/hooks/useDoubleClick.md +40 -0
- package/.agents/references/hooks/useGeolocation.md +37 -0
- package/.agents/references/hooks/useHasMounted.md +27 -0
- package/.agents/references/hooks/useIntersectionObserver.md +37 -0
- package/.agents/references/hooks/useInterval.md +40 -0
- package/.agents/references/hooks/useKeyboardHeight.md +33 -0
- package/.agents/references/hooks/useKeyboardListNavigation.md +58 -0
- package/.agents/references/hooks/useLongPress.md +48 -0
- package/.agents/references/hooks/usePreservedCallback.md +33 -0
- package/.agents/references/hooks/usePreservedReference.md +34 -0
- package/.agents/references/hooks/useRefEffect.md +35 -0
- package/.agents/references/hooks/useRelativeDateTime.md +35 -0
- package/.agents/references/hooks/useTimeout.md +42 -0
- package/.agents/references/hooks/useToggleState.md +39 -0
- package/.agents/references/hooks/useViewportHeight.md +26 -0
- package/.agents/references/hooks/useViewportMatch.md +32 -0
- package/.agents/references/utils/browserStorage.md +41 -0
- package/.agents/references/utils/buildContext.md +47 -0
- package/.agents/references/utils/clipboardShare.md +59 -0
- package/.agents/references/utils/floatingMotion.md +67 -0
- package/.agents/references/utils/getDeviceInfo.md +43 -0
- package/.agents/references/utils/isEditableKeyboardTarget.md +35 -0
- package/.agents/references/utils/mergeRefs.md +29 -0
- package/.agents/references/utils/seen.md +37 -0
- package/.agents/references/utils/subscribeKeyboardHeight.md +35 -0
- package/AGENTS.md +103 -396
- package/LICENSE +9 -0
- package/README.md +38 -0
- package/index.ts +97 -102
- package/package.json +27 -13
- package/{components → packages/components}/ScrolltoTop.tsx +3 -3
- package/{components → packages/components}/SwitchCase.tsx +7 -7
- package/packages/components/ViewportPortal.tsx +40 -0
- package/packages/cookie/cookie.shared.ts +138 -0
- package/packages/datetime/dateTime.client.ts +108 -0
- package/packages/datetime/dateTime.server.ts +108 -0
- package/packages/datetime/dateTime.shared.ts +358 -0
- package/packages/hooks/useAvoidKeyboard.ts +28 -0
- package/packages/hooks/useCheckInvisible.ts +25 -0
- package/packages/hooks/useCheckScroll.ts +19 -0
- package/packages/hooks/useClientDateTime.ts +49 -0
- package/packages/hooks/useDebounce.ts +118 -0
- package/packages/hooks/useDebouncedCallback.ts +69 -0
- package/packages/hooks/useDoubleClick.ts +53 -0
- package/packages/hooks/useGeolocation.ts +146 -0
- package/packages/hooks/useHasMounted.ts +13 -0
- package/packages/hooks/useIntersectionObserver.ts +34 -0
- package/packages/hooks/useInterval.ts +56 -0
- package/packages/hooks/useKeyboardHeight.ts +18 -0
- package/packages/hooks/useKeyboardListNavigation.ts +170 -0
- package/packages/hooks/useLongPress.ts +120 -0
- package/packages/hooks/usePreservedCallback.ts +21 -0
- package/packages/hooks/usePreservedReference.ts +21 -0
- package/packages/hooks/useRefEffect.ts +35 -0
- package/packages/hooks/useRelativeDateTime.ts +54 -0
- package/packages/hooks/useTimeout.ts +56 -0
- package/packages/hooks/useToggleState.ts +13 -0
- package/packages/hooks/useViewportHeight.ts +23 -0
- package/packages/hooks/useViewportMatch.ts +27 -0
- package/packages/utils/browserStorage.ts +59 -0
- package/packages/utils/buildContext.tsx +19 -0
- package/packages/utils/checkDevice.ts +74 -0
- package/packages/utils/clipboardShare.tsx +47 -0
- package/{utils → packages/utils}/clipboardShare.types.ts +7 -7
- package/packages/utils/floatingMotion.ts +99 -0
- package/packages/utils/keyboardTarget.ts +13 -0
- package/packages/utils/mergeRefs.ts +15 -0
- package/packages/utils/seen.ts +29 -0
- package/packages/utils/subscribeKeyboardHeight.ts +54 -0
- package/tests/components/ScrolltoTop.test.tsx +15 -0
- package/tests/components/SwitchCase.test.tsx +34 -0
- package/tests/components/ViewportPortal.test.tsx +42 -0
- package/tests/cookie/cookie.test.ts +156 -0
- package/tests/datetime/datetime.test.ts +250 -0
- package/tests/hooks/useAvoidKeyboard.test.ts +12 -0
- package/tests/hooks/useCheckInvisible.test.ts +28 -0
- package/tests/hooks/useCheckScroll.test.ts +23 -0
- package/tests/hooks/useClientDateTime.test.ts +14 -0
- package/tests/hooks/useDebounce.test.ts +71 -0
- package/tests/hooks/useDebouncedCallback.test.ts +34 -0
- package/tests/hooks/useDoubleClick.test.ts +31 -0
- package/tests/hooks/useGeolocation.test.ts +50 -0
- package/tests/hooks/useHasMounted.test.ts +10 -0
- package/tests/hooks/useIntersectionObserver.test.ts +13 -0
- package/tests/hooks/useInterval.test.ts +31 -0
- package/tests/hooks/useKeyboardHeight.test.ts +10 -0
- package/tests/hooks/useKeyboardListNavigation.test.ts +82 -0
- package/tests/hooks/useLongPress.test.ts +52 -0
- package/tests/hooks/usePreservedCallback.test.ts +35 -0
- package/tests/hooks/usePreservedReference.test.ts +35 -0
- package/tests/hooks/useRefEffect.test.ts +30 -0
- package/tests/hooks/useRelativeDateTime.test.ts +19 -0
- package/tests/hooks/useTimeout.test.ts +34 -0
- package/tests/hooks/useToggleState.test.ts +35 -0
- package/tests/hooks/useViewportHeight.test.ts +10 -0
- package/tests/hooks/useViewportMatch.test.ts +35 -0
- package/tests/setup.ts +9 -0
- package/tests/utils/browserStorage.test.ts +54 -0
- package/tests/utils/buildContext.test.tsx +27 -0
- package/tests/utils/checkDevice.test.ts +84 -0
- package/tests/utils/clipboardShare.test.ts +78 -0
- package/tests/utils/floatingMotion.test.ts +84 -0
- package/tests/utils/keyboardTarget.test.ts +44 -0
- package/tests/utils/mergeRefs.test.ts +18 -0
- package/tests/utils/seen.test.ts +31 -0
- package/tests/utils/subscribeKeyboardHeight.test.ts +50 -0
- package/tsconfig.json +16 -16
- package/vitest.config.ts +16 -0
- package/components/ViewportPortal.tsx +0 -40
- package/cookie/cookie.shared.ts +0 -142
- package/datetime/dateTime.client.ts +0 -108
- package/datetime/dateTime.server.ts +0 -108
- package/datetime/dateTime.shared.ts +0 -358
- package/hooks/useAvoidKeyboard.ts +0 -28
- package/hooks/useCheckInvisible.ts +0 -25
- package/hooks/useCheckScroll.ts +0 -19
- package/hooks/useClientDateTime.ts +0 -49
- package/hooks/useDebounce.ts +0 -118
- package/hooks/useDebouncedCallback.ts +0 -69
- package/hooks/useDoubleClick.ts +0 -53
- package/hooks/useGeolocation.ts +0 -146
- package/hooks/useHasMounted.ts +0 -13
- package/hooks/useIntersectionObserver.ts +0 -34
- package/hooks/useInterval.ts +0 -55
- package/hooks/useKeyboardHeight.ts +0 -18
- package/hooks/useKeyboardListNavigation.ts +0 -170
- package/hooks/useLongPress.ts +0 -120
- package/hooks/usePreservedCallback.ts +0 -21
- package/hooks/usePreservedReference.ts +0 -21
- package/hooks/useRefEffect.ts +0 -35
- package/hooks/useRelativeDateTime.ts +0 -54
- package/hooks/useTimeout.ts +0 -56
- package/hooks/useToggleState.ts +0 -13
- package/hooks/useViewportHeight.ts +0 -23
- package/hooks/useViewportMatch.ts +0 -33
- package/utils/browserStorage.ts +0 -59
- package/utils/buildContext.tsx +0 -19
- package/utils/checkDevice.ts +0 -74
- package/utils/clipboardShare.tsx +0 -47
- package/utils/floatingMotion.ts +0 -99
- package/utils/keyboardTarget.ts +0 -13
- package/utils/mergeRefs.ts +0 -15
- package/utils/seen.ts +0 -29
- 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,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
|
+
});
|