@musecat/functionkit 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.agents/references/components/ScrolltoTop.md +36 -0
  2. package/.agents/references/components/SwitchCase.md +47 -0
  3. package/.agents/references/components/ViewportPortal.md +34 -0
  4. package/.agents/references/cookie/cookie.md +47 -0
  5. package/.agents/references/datetime/dateTime.client.md +42 -0
  6. package/.agents/references/datetime/dateTime.server.md +42 -0
  7. package/.agents/references/datetime/dateTime.shared.md +86 -0
  8. package/.agents/references/hooks/useAvoidKeyboard.md +29 -0
  9. package/.agents/references/hooks/useCheckInvisible.md +36 -0
  10. package/.agents/references/hooks/useCheckScroll.md +36 -0
  11. package/.agents/references/hooks/useClientDateTime.md +39 -0
  12. package/.agents/references/hooks/useDebounce.md +46 -0
  13. package/.agents/references/hooks/useDebouncedCallback.md +34 -0
  14. package/.agents/references/hooks/useDoubleClick.md +40 -0
  15. package/.agents/references/hooks/useGeolocation.md +37 -0
  16. package/.agents/references/hooks/useHasMounted.md +27 -0
  17. package/.agents/references/hooks/useIntersectionObserver.md +37 -0
  18. package/.agents/references/hooks/useInterval.md +40 -0
  19. package/.agents/references/hooks/useKeyboardHeight.md +33 -0
  20. package/.agents/references/hooks/useKeyboardListNavigation.md +58 -0
  21. package/.agents/references/hooks/useLongPress.md +48 -0
  22. package/.agents/references/hooks/usePreservedCallback.md +33 -0
  23. package/.agents/references/hooks/usePreservedReference.md +34 -0
  24. package/.agents/references/hooks/useRefEffect.md +35 -0
  25. package/.agents/references/hooks/useRelativeDateTime.md +35 -0
  26. package/.agents/references/hooks/useTimeout.md +42 -0
  27. package/.agents/references/hooks/useToggleState.md +39 -0
  28. package/.agents/references/hooks/useViewportHeight.md +26 -0
  29. package/.agents/references/hooks/useViewportMatch.md +32 -0
  30. package/.agents/references/utils/browserStorage.md +41 -0
  31. package/.agents/references/utils/buildContext.md +47 -0
  32. package/.agents/references/utils/clipboardShare.md +59 -0
  33. package/.agents/references/utils/floatingMotion.md +67 -0
  34. package/.agents/references/utils/getDeviceInfo.md +43 -0
  35. package/.agents/references/utils/isEditableKeyboardTarget.md +35 -0
  36. package/.agents/references/utils/mergeRefs.md +29 -0
  37. package/.agents/references/utils/seen.md +37 -0
  38. package/.agents/references/utils/subscribeKeyboardHeight.md +35 -0
  39. package/AGENTS.md +103 -396
  40. package/README.md +18 -5
  41. package/index.ts +63 -68
  42. package/package.json +27 -13
  43. package/packages/components/ScrolltoTop.tsx +3 -3
  44. package/packages/components/SwitchCase.tsx +7 -7
  45. package/packages/components/ViewportPortal.tsx +23 -23
  46. package/packages/cookie/cookie.shared.ts +97 -101
  47. package/packages/datetime/dateTime.client.ts +86 -86
  48. package/packages/datetime/dateTime.server.ts +86 -86
  49. package/packages/datetime/dateTime.shared.ts +281 -281
  50. package/packages/hooks/useAvoidKeyboard.ts +15 -15
  51. package/packages/hooks/useCheckInvisible.ts +16 -16
  52. package/packages/hooks/useCheckScroll.ts +10 -10
  53. package/packages/hooks/useClientDateTime.ts +33 -33
  54. package/packages/hooks/useDebounce.ts +95 -95
  55. package/packages/hooks/useDebouncedCallback.ts +45 -45
  56. package/packages/hooks/useDoubleClick.ts +43 -43
  57. package/packages/hooks/useGeolocation.ts +130 -130
  58. package/packages/hooks/useHasMounted.ts +5 -5
  59. package/packages/hooks/useIntersectionObserver.ts +20 -20
  60. package/packages/hooks/useInterval.ts +46 -45
  61. package/packages/hooks/useKeyboardHeight.ts +9 -9
  62. package/packages/hooks/useKeyboardListNavigation.ts +156 -156
  63. package/packages/hooks/useLongPress.ts +98 -98
  64. package/packages/hooks/usePreservedCallback.ts +12 -12
  65. package/packages/hooks/usePreservedReference.ts +10 -10
  66. package/packages/hooks/useRefEffect.ts +19 -19
  67. package/packages/hooks/useRelativeDateTime.ts +37 -37
  68. package/packages/hooks/useTimeout.ts +41 -41
  69. package/packages/hooks/useToggleState.ts +5 -5
  70. package/packages/hooks/useViewportHeight.ts +13 -13
  71. package/packages/hooks/useViewportMatch.ts +16 -22
  72. package/packages/utils/browserStorage.ts +26 -26
  73. package/packages/utils/buildContext.tsx +10 -10
  74. package/packages/utils/checkDevice.ts +64 -64
  75. package/packages/utils/clipboardShare.tsx +31 -31
  76. package/packages/utils/clipboardShare.types.ts +7 -7
  77. package/packages/utils/floatingMotion.ts +71 -71
  78. package/packages/utils/keyboardTarget.ts +9 -9
  79. package/packages/utils/mergeRefs.ts +9 -9
  80. package/packages/utils/seen.ts +15 -15
  81. package/packages/utils/subscribeKeyboardHeight.ts +46 -46
  82. package/tests/components/ScrolltoTop.test.tsx +15 -0
  83. package/tests/components/SwitchCase.test.tsx +34 -0
  84. package/tests/components/ViewportPortal.test.tsx +42 -0
  85. package/tests/cookie/cookie.test.ts +156 -0
  86. package/tests/datetime/datetime.test.ts +250 -0
  87. package/tests/hooks/useAvoidKeyboard.test.ts +12 -0
  88. package/tests/hooks/useCheckInvisible.test.ts +28 -0
  89. package/tests/hooks/useCheckScroll.test.ts +23 -0
  90. package/tests/hooks/useClientDateTime.test.ts +14 -0
  91. package/tests/hooks/useDebounce.test.ts +71 -0
  92. package/tests/hooks/useDebouncedCallback.test.ts +34 -0
  93. package/tests/hooks/useDoubleClick.test.ts +31 -0
  94. package/tests/hooks/useGeolocation.test.ts +50 -0
  95. package/tests/hooks/useHasMounted.test.ts +10 -0
  96. package/tests/hooks/useIntersectionObserver.test.ts +13 -0
  97. package/tests/hooks/useInterval.test.ts +31 -0
  98. package/tests/hooks/useKeyboardHeight.test.ts +10 -0
  99. package/tests/hooks/useKeyboardListNavigation.test.ts +82 -0
  100. package/tests/hooks/useLongPress.test.ts +52 -0
  101. package/tests/hooks/usePreservedCallback.test.ts +35 -0
  102. package/tests/hooks/usePreservedReference.test.ts +35 -0
  103. package/tests/hooks/useRefEffect.test.ts +30 -0
  104. package/tests/hooks/useRelativeDateTime.test.ts +19 -0
  105. package/tests/hooks/useTimeout.test.ts +34 -0
  106. package/tests/hooks/useToggleState.test.ts +35 -0
  107. package/tests/hooks/useViewportHeight.test.ts +10 -0
  108. package/tests/hooks/useViewportMatch.test.ts +35 -0
  109. package/tests/setup.ts +9 -0
  110. package/tests/utils/browserStorage.test.ts +54 -0
  111. package/tests/utils/buildContext.test.tsx +27 -0
  112. package/tests/utils/checkDevice.test.ts +84 -0
  113. package/tests/utils/clipboardShare.test.ts +78 -0
  114. package/tests/utils/floatingMotion.test.ts +84 -0
  115. package/tests/utils/keyboardTarget.test.ts +44 -0
  116. package/tests/utils/mergeRefs.test.ts +18 -0
  117. package/tests/utils/seen.test.ts +31 -0
  118. package/tests/utils/subscribeKeyboardHeight.test.ts +50 -0
  119. package/tsconfig.json +16 -16
  120. package/vitest.config.ts +16 -0
@@ -0,0 +1,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,9 @@
1
+ import "@testing-library/jest-dom";
2
+
3
+ class ResizeObserverMock {
4
+ observe() {}
5
+ unobserve() {}
6
+ disconnect() {}
7
+ }
8
+
9
+ window.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver;
@@ -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
+ });