@musecat/functionkit 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/README.md +18 -5
- package/index.ts +63 -68
- package/package.json +27 -13
- package/packages/components/ScrolltoTop.tsx +3 -3
- package/packages/components/SwitchCase.tsx +7 -7
- package/packages/components/ViewportPortal.tsx +23 -23
- package/packages/cookie/cookie.shared.ts +97 -101
- package/packages/datetime/dateTime.client.ts +86 -86
- package/packages/datetime/dateTime.server.ts +86 -86
- package/packages/datetime/dateTime.shared.ts +281 -281
- package/packages/hooks/useAvoidKeyboard.ts +15 -15
- package/packages/hooks/useCheckInvisible.ts +16 -16
- package/packages/hooks/useCheckScroll.ts +10 -10
- package/packages/hooks/useClientDateTime.ts +33 -33
- package/packages/hooks/useDebounce.ts +95 -95
- package/packages/hooks/useDebouncedCallback.ts +45 -45
- package/packages/hooks/useDoubleClick.ts +43 -43
- package/packages/hooks/useGeolocation.ts +130 -130
- package/packages/hooks/useHasMounted.ts +5 -5
- package/packages/hooks/useIntersectionObserver.ts +20 -20
- package/packages/hooks/useInterval.ts +46 -45
- package/packages/hooks/useKeyboardHeight.ts +9 -9
- package/packages/hooks/useKeyboardListNavigation.ts +156 -156
- package/packages/hooks/useLongPress.ts +98 -98
- package/packages/hooks/usePreservedCallback.ts +12 -12
- package/packages/hooks/usePreservedReference.ts +10 -10
- package/packages/hooks/useRefEffect.ts +19 -19
- package/packages/hooks/useRelativeDateTime.ts +37 -37
- package/packages/hooks/useTimeout.ts +41 -41
- package/packages/hooks/useToggleState.ts +5 -5
- package/packages/hooks/useViewportHeight.ts +13 -13
- package/packages/hooks/useViewportMatch.ts +16 -22
- package/packages/utils/browserStorage.ts +26 -26
- package/packages/utils/buildContext.tsx +10 -10
- package/packages/utils/checkDevice.ts +64 -64
- package/packages/utils/clipboardShare.tsx +31 -31
- package/packages/utils/clipboardShare.types.ts +7 -7
- package/packages/utils/floatingMotion.ts +71 -71
- package/packages/utils/keyboardTarget.ts +9 -9
- package/packages/utils/mergeRefs.ts +9 -9
- package/packages/utils/seen.ts +15 -15
- package/packages/utils/subscribeKeyboardHeight.ts +46 -46
- 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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, test, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { NavigatorClipboard, NavigatorShare } from "@/packages/utils/clipboardShare";
|
|
3
|
+
|
|
4
|
+
describe("NavigatorClipboard", () => {
|
|
5
|
+
test("resolves with success: true on write", async () => {
|
|
6
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
7
|
+
value: { writeText: vi.fn(() => Promise.resolve()) },
|
|
8
|
+
writable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const result = await NavigatorClipboard({ text: "hello" });
|
|
13
|
+
expect(result).toEqual({ success: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("resolves with success: false on failure", async () => {
|
|
17
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
18
|
+
value: { writeText: vi.fn(() => Promise.reject(new Error("denied"))) },
|
|
19
|
+
writable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = await NavigatorClipboard({ text: "hello" });
|
|
24
|
+
expect(result).toEqual({ success: false });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("NavigatorShare", () => {
|
|
29
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
30
|
+
|
|
31
|
+
test("uses Web Share API when available", async () => {
|
|
32
|
+
Object.defineProperty(navigator, "share", {
|
|
33
|
+
value: vi.fn(() => Promise.resolve()),
|
|
34
|
+
writable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
38
|
+
value: { writeText: vi.fn() },
|
|
39
|
+
writable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const result = await NavigatorShare({ title: "Test", text: "Hello", link: "https://example.com" });
|
|
44
|
+
expect(result).toEqual({ success: true, method: "share" });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("falls back to clipboard when share is unavailable", async () => {
|
|
48
|
+
Object.defineProperty(navigator, "share", {
|
|
49
|
+
value: undefined,
|
|
50
|
+
writable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
});
|
|
53
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
54
|
+
value: { writeText: vi.fn(() => Promise.resolve()) },
|
|
55
|
+
writable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await NavigatorShare({ title: "Test", text: "Hello", link: "https://example.com" });
|
|
60
|
+
expect(result).toEqual({ success: true, method: "clipboard" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("returns unsupported when neither share nor clipboard", async () => {
|
|
64
|
+
Object.defineProperty(navigator, "share", {
|
|
65
|
+
value: undefined,
|
|
66
|
+
writable: true,
|
|
67
|
+
configurable: true,
|
|
68
|
+
});
|
|
69
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
70
|
+
value: undefined,
|
|
71
|
+
writable: true,
|
|
72
|
+
configurable: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await NavigatorShare({ title: "Test", text: "Hello", link: "https://example.com" });
|
|
76
|
+
expect(result).toEqual({ success: false, method: "unsupported" });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getFloatingMotionPreset,
|
|
4
|
+
getFloatingTransformOrigin,
|
|
5
|
+
getFloatingHiddenTransform,
|
|
6
|
+
} from "@/packages/utils/floatingMotion";
|
|
7
|
+
|
|
8
|
+
describe("getFloatingMotionPreset", () => {
|
|
9
|
+
test("returns anchored preset", () => {
|
|
10
|
+
const preset = getFloatingMotionPreset("anchored");
|
|
11
|
+
expect(preset.enterMs).toBe(230);
|
|
12
|
+
expect(preset.exitMs).toBe(170);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("returns modal-center preset", () => {
|
|
16
|
+
const preset = getFloatingMotionPreset("modal-center");
|
|
17
|
+
expect(preset.enterMs).toBe(340);
|
|
18
|
+
expect(preset.exitMs).toBe(260);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("returns center-selected preset", () => {
|
|
22
|
+
const preset = getFloatingMotionPreset("center-selected");
|
|
23
|
+
expect(preset.enterMs).toBe(190);
|
|
24
|
+
expect(preset.exitMs).toBe(140);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns mobile-sheet preset", () => {
|
|
28
|
+
const preset = getFloatingMotionPreset("mobile-sheet");
|
|
29
|
+
expect(preset.enterMs).toBe(360);
|
|
30
|
+
expect(preset.exitMs).toBe(280);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("getFloatingTransformOrigin", () => {
|
|
35
|
+
test("returns default when no placement", () => {
|
|
36
|
+
expect(getFloatingTransformOrigin()).toBe("top center");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("returns correct origin for top-left", () => {
|
|
40
|
+
expect(getFloatingTransformOrigin("top-left")).toBe("bottom left");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("returns correct origin for bottom-right", () => {
|
|
44
|
+
expect(getFloatingTransformOrigin("bottom-right")).toBe("top right");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("returns correct origin for middle-center", () => {
|
|
48
|
+
expect(getFloatingTransformOrigin("middle-center")).toBe("center center");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("getFloatingHiddenTransform", () => {
|
|
53
|
+
test("returns mobile-sheet transform", () => {
|
|
54
|
+
expect(getFloatingHiddenTransform({ mode: "mobile-sheet" })).toBe("translateY(1.8rem) scale(1)");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("returns modal-center transform", () => {
|
|
58
|
+
expect(getFloatingHiddenTransform({ mode: "modal-center" })).toBe("translateY(.8rem) scale(.94)");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("returns center-selected transform", () => {
|
|
62
|
+
expect(getFloatingHiddenTransform({ mode: "center-selected" })).toBe("translateY(.45rem) scale(.99)");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("anchored with no placement", () => {
|
|
66
|
+
expect(getFloatingHiddenTransform({ mode: "anchored" })).toBe("translateY(.4rem) scale(.975)");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("anchored with top placement", () => {
|
|
70
|
+
expect(getFloatingHiddenTransform({ mode: "anchored", placement: "top-left" })).toBe("translateY(.4rem) scale(.975)");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("anchored with bottom placement", () => {
|
|
74
|
+
expect(getFloatingHiddenTransform({ mode: "anchored", placement: "bottom-center" })).toBe("translateY(-.4rem) scale(.975)");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("anchored with middle-left placement", () => {
|
|
78
|
+
expect(getFloatingHiddenTransform({ mode: "anchored", placement: "middle-left" })).toBe("translateX(.4rem) scale(.975)");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("anchored with middle-right placement", () => {
|
|
82
|
+
expect(getFloatingHiddenTransform({ mode: "anchored", placement: "middle-right" })).toBe("translateX(-.4rem) scale(.975)");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { isEditableKeyboardTarget } from "@/packages/utils/keyboardTarget";
|
|
3
|
+
|
|
4
|
+
describe("isEditableKeyboardTarget", () => {
|
|
5
|
+
test("returns true for contentEditable", () => {
|
|
6
|
+
const el = document.createElement("div");
|
|
7
|
+
Object.defineProperty(el, "isContentEditable", { value: true });
|
|
8
|
+
expect(isEditableKeyboardTarget(el)).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("returns true for TEXTAREA", () => {
|
|
12
|
+
const el = document.createElement("textarea");
|
|
13
|
+
expect(isEditableKeyboardTarget(el)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("returns true for text INPUT", () => {
|
|
17
|
+
const el = document.createElement("input");
|
|
18
|
+
el.type = "text";
|
|
19
|
+
expect(isEditableKeyboardTarget(el)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("returns false for button INPUT", () => {
|
|
23
|
+
const el = document.createElement("input");
|
|
24
|
+
el.type = "button";
|
|
25
|
+
expect(isEditableKeyboardTarget(el)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns false for checkbox INPUT", () => {
|
|
29
|
+
const el = document.createElement("input");
|
|
30
|
+
el.type = "checkbox";
|
|
31
|
+
expect(isEditableKeyboardTarget(el)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("returns false for radio INPUT", () => {
|
|
35
|
+
const el = document.createElement("input");
|
|
36
|
+
el.type = "radio";
|
|
37
|
+
expect(isEditableKeyboardTarget(el)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("returns false for regular div", () => {
|
|
41
|
+
const el = document.createElement("div");
|
|
42
|
+
expect(isEditableKeyboardTarget(el)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|