@musecat/functionkit 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/references/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 +41 -0
- package/.agents/references/hooks/useInterval.md +44 -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 +37 -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/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/.github/workflows/ci.yml +20 -0
- package/.local/state/gh/device-id +1 -0
- package/.prettierrc +8 -0
- package/AGENTS.md +117 -396
- package/CODE_OF_CONDUCT.md +55 -0
- package/CONTRIBUTING.md +39 -0
- package/README.md +18 -5
- package/biome.json +25 -0
- package/index.ts +63 -68
- package/package.json +46 -13
- package/packages/components/ScrolltoTop.tsx +3 -3
- package/packages/components/SwitchCase.tsx +7 -7
- package/packages/components/ViewportPortal.tsx +22 -23
- package/packages/cookie/cookie.shared.ts +80 -110
- package/packages/cookie/cookieNames.shared.ts +9 -0
- package/packages/datetime/dateTime.client.ts +84 -86
- package/packages/datetime/dateTime.server.ts +84 -86
- package/packages/datetime/dateTime.shared.ts +249 -287
- 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 +24 -37
- package/packages/hooks/useDebounce.ts +90 -95
- package/packages/hooks/useDebouncedCallback.ts +45 -45
- package/packages/hooks/useDoubleClick.ts +43 -43
- package/packages/hooks/useGeolocation.ts +126 -130
- package/packages/hooks/useHasMounted.ts +5 -5
- package/packages/hooks/useIntersectionObserver.ts +20 -20
- package/packages/hooks/useInterval.ts +45 -45
- package/packages/hooks/useKeyboardHeight.ts +9 -9
- package/packages/hooks/useKeyboardListNavigation.ts +130 -158
- package/packages/hooks/useLongPress.ts +87 -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 +36 -39
- package/packages/hooks/useTimeout.ts +41 -41
- package/packages/hooks/useToggleState.ts +5 -5
- package/packages/hooks/useViewportHeight.ts +12 -13
- package/packages/hooks/useViewportMatch.ts +13 -23
- package/packages/utils/browserStorage.ts +26 -26
- package/packages/utils/buildContext.tsx +10 -11
- 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 -77
- 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 +49 -46
- package/tests/components/ScrolltoTop.test.tsx +15 -0
- package/tests/components/SwitchCase.test.tsx +33 -0
- package/tests/components/ViewportPortal.test.tsx +46 -0
- package/tests/cookie/cookie.test.ts +192 -0
- package/tests/datetime/datetime.test.ts +461 -0
- package/tests/hooks/useAvoidKeyboard.test.ts +43 -0
- package/tests/hooks/useCheckInvisible.test.ts +59 -0
- package/tests/hooks/useCheckScroll.test.ts +23 -0
- package/tests/hooks/useClientDateTime.test.ts +12 -0
- package/tests/hooks/useDebounce.test.ts +96 -0
- package/tests/hooks/useDebouncedCallback.test.ts +63 -0
- package/tests/hooks/useDoubleClick.test.ts +82 -0
- package/tests/hooks/useGeolocation.test.ts +201 -0
- package/tests/hooks/useHasMounted.test.ts +10 -0
- package/tests/hooks/useIntersectionObserver.test.ts +78 -0
- package/tests/hooks/useInterval.test.ts +56 -0
- package/tests/hooks/useKeyboardHeight.test.ts +10 -0
- package/tests/hooks/useKeyboardListNavigation.test.ts +404 -0
- package/tests/hooks/useLongPress.test.ts +214 -0
- package/tests/hooks/usePreservedCallback.test.ts +33 -0
- package/tests/hooks/usePreservedReference.test.ts +42 -0
- package/tests/hooks/useRefEffect.test.ts +67 -0
- package/tests/hooks/useRelativeDateTime.test.ts +21 -0
- package/tests/hooks/useTimeout.test.ts +72 -0
- package/tests/hooks/useToggleState.test.ts +43 -0
- package/tests/hooks/useViewportHeight.test.ts +32 -0
- package/tests/hooks/useViewportMatch.test.ts +35 -0
- package/tests/setup.ts +9 -0
- package/tests/utils/browserStorage.test.ts +114 -0
- package/tests/utils/buildContext.test.tsx +26 -0
- package/tests/utils/checkDevice.test.ts +140 -0
- package/tests/utils/clipboardShare.test.ts +152 -0
- package/tests/utils/floatingMotion.test.ts +135 -0
- package/tests/utils/keyboardTarget.test.ts +85 -0
- package/tests/utils/mergeRefs.test.ts +50 -0
- package/tests/utils/seen.test.ts +40 -0
- package/tests/utils/subscribeKeyboardHeight.test.ts +172 -0
- package/tsconfig.json +16 -16
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test, vi } from "vitest";
|
|
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(() => {
|
|
13
|
+
(result.current as (el: HTMLElement | null) => void)(element);
|
|
14
|
+
});
|
|
15
|
+
expect(callback).toHaveBeenCalledWith(element);
|
|
16
|
+
|
|
17
|
+
act(() => {
|
|
18
|
+
(result.current as (el: HTMLElement | null) => void)(null);
|
|
19
|
+
});
|
|
20
|
+
expect(cleanup).toHaveBeenCalledOnce();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("stores cleanup when callback returns a function", () => {
|
|
24
|
+
const cleanup = vi.fn();
|
|
25
|
+
const callback = vi.fn(() => cleanup);
|
|
26
|
+
const { result } = renderHook(() => useRefEffect(callback, []));
|
|
27
|
+
|
|
28
|
+
const element = document.createElement("div");
|
|
29
|
+
act(() => {
|
|
30
|
+
(result.current as (el: HTMLElement | null) => void)(element);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
act(() => {
|
|
34
|
+
(result.current as (el: HTMLElement | null) => void)(null);
|
|
35
|
+
});
|
|
36
|
+
expect(cleanup).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("handles callback returning undefined (no cleanup)", () => {
|
|
40
|
+
const { result } = renderHook(() => useRefEffect(() => {}, []));
|
|
41
|
+
|
|
42
|
+
const element = document.createElement("div");
|
|
43
|
+
act(() => {
|
|
44
|
+
(result.current as (el: HTMLElement | null) => void)(element);
|
|
45
|
+
});
|
|
46
|
+
// Should not throw when cleanup is undefined
|
|
47
|
+
act(() => {
|
|
48
|
+
(result.current as (el: HTMLElement | null) => void)(null);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("calls cleanup before setting new element", () => {
|
|
53
|
+
const cleanup = vi.fn();
|
|
54
|
+
const callback = vi.fn(() => cleanup);
|
|
55
|
+
const { result } = renderHook(() => useRefEffect(callback, []));
|
|
56
|
+
|
|
57
|
+
const el1 = document.createElement("div");
|
|
58
|
+
const el2 = document.createElement("span");
|
|
59
|
+
act(() => {
|
|
60
|
+
(result.current as (el: HTMLElement | null) => void)(el1);
|
|
61
|
+
});
|
|
62
|
+
act(() => {
|
|
63
|
+
(result.current as (el: HTMLElement | null) => void)(el2);
|
|
64
|
+
});
|
|
65
|
+
expect(cleanup).toHaveBeenCalledOnce();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { useRelativeDateTime } from "@/packages/hooks/useRelativeDateTime";
|
|
4
|
+
|
|
5
|
+
describe("useRelativeDateTime", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("returns relative time text", () => {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const fiveMinAgo = new Date(now - 5 * 60 * 1000).toISOString();
|
|
16
|
+
const { result } = renderHook(() => useRelativeDateTime(fiveMinAgo, { locale: "ko" }));
|
|
17
|
+
expect(result.current.ready).toBe(true);
|
|
18
|
+
expect(result.current.isRelative).toBe(true);
|
|
19
|
+
expect(result.current.text).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { useTimeout } from "@/packages/hooks/useTimeout";
|
|
4
|
+
|
|
5
|
+
describe("useTimeout", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("calls callback after delay", () => {
|
|
14
|
+
const fn = vi.fn();
|
|
15
|
+
renderHook(() => useTimeout(fn, 100));
|
|
16
|
+
expect(fn).not.toHaveBeenCalled();
|
|
17
|
+
vi.advanceTimersByTime(100);
|
|
18
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("does not call when disabled", () => {
|
|
22
|
+
const fn = vi.fn();
|
|
23
|
+
renderHook(() => useTimeout(fn, 100, { enabled: false }));
|
|
24
|
+
vi.advanceTimersByTime(200);
|
|
25
|
+
expect(fn).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("clear cancels pending timeout", () => {
|
|
29
|
+
const fn = vi.fn();
|
|
30
|
+
const { result } = renderHook(() => useTimeout(fn, 100));
|
|
31
|
+
|
|
32
|
+
act(() => {
|
|
33
|
+
result.current.clear();
|
|
34
|
+
});
|
|
35
|
+
vi.advanceTimersByTime(100);
|
|
36
|
+
expect(fn).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("start and clear controls", () => {
|
|
40
|
+
const fn = vi.fn();
|
|
41
|
+
const { result } = renderHook(() => useTimeout(fn, 1000, { enabled: false }));
|
|
42
|
+
expect(result.current.isPending()).toBe(false);
|
|
43
|
+
act(() => {
|
|
44
|
+
result.current.start(50);
|
|
45
|
+
});
|
|
46
|
+
expect(result.current.isPending()).toBe(true);
|
|
47
|
+
vi.advanceTimersByTime(50);
|
|
48
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
49
|
+
expect(result.current.isPending()).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("reset restarts the timeout", () => {
|
|
53
|
+
const fn = vi.fn();
|
|
54
|
+
const { result } = renderHook(() => useTimeout(fn, 500));
|
|
55
|
+
vi.advanceTimersByTime(200);
|
|
56
|
+
act(() => {
|
|
57
|
+
result.current.reset(100);
|
|
58
|
+
});
|
|
59
|
+
vi.advanceTimersByTime(100);
|
|
60
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("start overrides default delay", () => {
|
|
64
|
+
const fn = vi.fn();
|
|
65
|
+
const { result } = renderHook(() => useTimeout(fn, 500, { enabled: false }));
|
|
66
|
+
act(() => {
|
|
67
|
+
result.current.start(50);
|
|
68
|
+
});
|
|
69
|
+
vi.advanceTimersByTime(50);
|
|
70
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
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(() => {
|
|
19
|
+
result.current.setTrue();
|
|
20
|
+
});
|
|
21
|
+
expect(result.current.value).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("setFalse sets value to false", () => {
|
|
25
|
+
const { result } = renderHook(() => useToggleState(true));
|
|
26
|
+
act(() => {
|
|
27
|
+
result.current.setFalse();
|
|
28
|
+
});
|
|
29
|
+
expect(result.current.value).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("toggle switches value", () => {
|
|
33
|
+
const { result } = renderHook(() => useToggleState(false));
|
|
34
|
+
act(() => {
|
|
35
|
+
result.current.toggle();
|
|
36
|
+
});
|
|
37
|
+
expect(result.current.value).toBe(true);
|
|
38
|
+
act(() => {
|
|
39
|
+
result.current.toggle();
|
|
40
|
+
});
|
|
41
|
+
expect(result.current.value).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test, vi } from "vitest";
|
|
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
|
+
|
|
11
|
+
test("updates height on window resize", () => {
|
|
12
|
+
const { result } = renderHook(() => useViewportHeight());
|
|
13
|
+
|
|
14
|
+
act(() => {
|
|
15
|
+
window.innerHeight = 500;
|
|
16
|
+
window.dispatchEvent(new Event("resize"));
|
|
17
|
+
});
|
|
18
|
+
expect(result.current.height).toBe(500);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("removes event listeners on unmount", () => {
|
|
22
|
+
const addResize = vi.spyOn(window, "addEventListener");
|
|
23
|
+
const removeResize = vi.spyOn(window, "removeEventListener");
|
|
24
|
+
|
|
25
|
+
const { unmount } = renderHook(() => useViewportHeight());
|
|
26
|
+
unmount();
|
|
27
|
+
|
|
28
|
+
expect(removeResize).toHaveBeenCalledWith("resize", expect.any(Function));
|
|
29
|
+
addResize.mockRestore();
|
|
30
|
+
removeResize.mockRestore();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
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,114 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getLocalStorage,
|
|
4
|
+
getSessionStorage,
|
|
5
|
+
removeLocalStorage,
|
|
6
|
+
removeSessionStorage,
|
|
7
|
+
updateLocalStorage,
|
|
8
|
+
updateSessionStorage,
|
|
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) => {
|
|
22
|
+
store[key] = value.toString();
|
|
23
|
+
}),
|
|
24
|
+
removeItem: vi.fn((key) => {
|
|
25
|
+
delete store[key];
|
|
26
|
+
}),
|
|
27
|
+
clear: vi.fn(() => {
|
|
28
|
+
Object.keys(store).forEach((k) => delete store[k]);
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
vi.stubGlobal("localStorage", createMockStorage(mockStorage));
|
|
33
|
+
vi.stubGlobal("sessionStorage", createMockStorage(mockStorage2));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.unstubAllGlobals();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("localStorage operations", () => {
|
|
41
|
+
updateLocalStorage("foo", { bar: "baz" });
|
|
42
|
+
expect(localStorage.getItem("foo")).toBe('{"bar":"baz"}');
|
|
43
|
+
expect(getLocalStorage("foo")).toEqual({ bar: "baz" });
|
|
44
|
+
|
|
45
|
+
removeLocalStorage("foo");
|
|
46
|
+
expect(getLocalStorage("foo")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("sessionStorage operations", () => {
|
|
50
|
+
updateSessionStorage("foo", { bar: "baz" });
|
|
51
|
+
expect(sessionStorage.getItem("foo")).toBe('{"bar":"baz"}');
|
|
52
|
+
expect(getSessionStorage("foo")).toEqual({ bar: "baz" });
|
|
53
|
+
|
|
54
|
+
removeSessionStorage("foo");
|
|
55
|
+
expect(getSessionStorage("foo")).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("handles invalid json gracefully", () => {
|
|
59
|
+
localStorage.setItem("invalid", "not-json");
|
|
60
|
+
expect(getLocalStorage("invalid")).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("returns null when window is undefined (SSR)", () => {
|
|
64
|
+
const win = global.window;
|
|
65
|
+
// @ts-expect-error
|
|
66
|
+
delete global.window;
|
|
67
|
+
expect(getLocalStorage("foo")).toBeNull();
|
|
68
|
+
expect(getSessionStorage("foo")).toBeNull();
|
|
69
|
+
updateLocalStorage("foo", "bar");
|
|
70
|
+
global.window = win;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("handles storage setItem throwing", () => {
|
|
74
|
+
const storage = localStorage;
|
|
75
|
+
vi.spyOn(storage, "setItem").mockImplementation(() => {
|
|
76
|
+
throw new Error("QuotaExceededError");
|
|
77
|
+
});
|
|
78
|
+
updateLocalStorage("foo", "bar");
|
|
79
|
+
expect(storage.setItem).toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("removeStorage handles missing storage gracefully", () => {
|
|
83
|
+
const win = global.window;
|
|
84
|
+
// @ts-expect-error
|
|
85
|
+
delete global.window;
|
|
86
|
+
removeLocalStorage("foo");
|
|
87
|
+
removeSessionStorage("foo");
|
|
88
|
+
global.window = win;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("getLocalStorage returns null when getItem throws", () => {
|
|
92
|
+
const storage = localStorage;
|
|
93
|
+
vi.spyOn(storage, "getItem").mockImplementation(() => {
|
|
94
|
+
throw new Error("StorageError");
|
|
95
|
+
});
|
|
96
|
+
expect(getLocalStorage("foo")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("removeLocalStorage handles removeItem throwing", () => {
|
|
100
|
+
const storage = localStorage;
|
|
101
|
+
vi.spyOn(storage, "removeItem").mockImplementation(() => {
|
|
102
|
+
throw new Error("StorageError");
|
|
103
|
+
});
|
|
104
|
+
expect(() => removeLocalStorage("foo")).not.toThrow();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("getLocalStorage returns null for non-existent key", () => {
|
|
108
|
+
expect(getLocalStorage("non-existent-key")).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("sessionStorage returns null for non-existent key", () => {
|
|
112
|
+
expect(getSessionStorage("non-existent-key")).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { buildContext } from "@/packages/utils/buildContext";
|
|
4
|
+
|
|
5
|
+
describe("buildContext", () => {
|
|
6
|
+
const [Provider, useValue] = buildContext<string>("default");
|
|
7
|
+
|
|
8
|
+
function TestComponent() {
|
|
9
|
+
const val = useValue();
|
|
10
|
+
return <div data-testid="val">{val}</div>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("returns default value when not wrapped in provider", () => {
|
|
14
|
+
render(<TestComponent />);
|
|
15
|
+
expect(screen.getByTestId("val").textContent).toBe("default");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("returns provided value", () => {
|
|
19
|
+
render(
|
|
20
|
+
<Provider value="provided">
|
|
21
|
+
<TestComponent />
|
|
22
|
+
</Provider>,
|
|
23
|
+
);
|
|
24
|
+
expect(screen.getByTestId("val").textContent).toBe("provided");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } 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:
|
|
27
|
+
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
|
|
28
|
+
writable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
});
|
|
31
|
+
const info = getDeviceInfo();
|
|
32
|
+
expect(info.isMobile).toBe(true);
|
|
33
|
+
expect(info.isAndroid).toBe(true);
|
|
34
|
+
expect(info.browser).toBe("chrome");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("detects iOS Safari", () => {
|
|
38
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
39
|
+
value:
|
|
40
|
+
"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",
|
|
41
|
+
writable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
});
|
|
44
|
+
const info = getDeviceInfo();
|
|
45
|
+
expect(info.isMobile).toBe(true);
|
|
46
|
+
expect(info.isIOS).toBe(true);
|
|
47
|
+
expect(info.isSafari).toBe(true);
|
|
48
|
+
expect(info.isIOSSafari).toBe(true);
|
|
49
|
+
expect(info.browser).toBe("safari");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("detects desktop Chrome", () => {
|
|
53
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
54
|
+
value:
|
|
55
|
+
"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",
|
|
56
|
+
writable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
});
|
|
59
|
+
const info = getDeviceInfo();
|
|
60
|
+
expect(info.isMobile).toBe(false);
|
|
61
|
+
expect(info.browser).toBe("chrome");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("detects Mac Safari", () => {
|
|
65
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
66
|
+
value:
|
|
67
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/604.1",
|
|
68
|
+
writable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
});
|
|
71
|
+
const info = getDeviceInfo();
|
|
72
|
+
expect(info.isMobile).toBe(false);
|
|
73
|
+
expect(info.isMacSafari).toBe(true);
|
|
74
|
+
expect(info.isSafari).toBe(true);
|
|
75
|
+
expect(info.browser).toBe("safari");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("detects Firefox browser", () => {
|
|
79
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
80
|
+
value: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0",
|
|
81
|
+
writable: true,
|
|
82
|
+
configurable: true,
|
|
83
|
+
});
|
|
84
|
+
const info = getDeviceInfo();
|
|
85
|
+
expect(info.browser).toBe("firefox");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("detects Edge browser", () => {
|
|
89
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
90
|
+
value:
|
|
91
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/44.18362.1.0",
|
|
92
|
+
writable: true,
|
|
93
|
+
configurable: true,
|
|
94
|
+
});
|
|
95
|
+
const info = getDeviceInfo();
|
|
96
|
+
expect(info.browser).toBe("edge");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("returns unknown browser for unrecognized UA", () => {
|
|
100
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
101
|
+
value: "SomeUnknownBrowser/1.0",
|
|
102
|
+
writable: true,
|
|
103
|
+
configurable: true,
|
|
104
|
+
});
|
|
105
|
+
const info = getDeviceInfo();
|
|
106
|
+
expect(info.browser).toBe("unknown");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("detects Samsung Browser", () => {
|
|
110
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
111
|
+
value:
|
|
112
|
+
"Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/120.0.6099.230 Mobile Safari/537.36",
|
|
113
|
+
writable: true,
|
|
114
|
+
configurable: true,
|
|
115
|
+
});
|
|
116
|
+
const info = getDeviceInfo();
|
|
117
|
+
expect(info.isSamsungBrowser).toBe(true);
|
|
118
|
+
expect(info.browser).toBe("samsung");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("detects isTouchDevice correctly", () => {
|
|
122
|
+
const hasTouch = "ontouchstart" in window;
|
|
123
|
+
const info = getDeviceInfo();
|
|
124
|
+
expect(info.isTouchDevice).toBe(hasTouch);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("detects desktop Safari correctly distinguishes from mobile", () => {
|
|
128
|
+
Object.defineProperty(navigator, "userAgent", {
|
|
129
|
+
value:
|
|
130
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/604.1",
|
|
131
|
+
writable: true,
|
|
132
|
+
configurable: true,
|
|
133
|
+
});
|
|
134
|
+
const info = getDeviceInfo();
|
|
135
|
+
expect(info.isSafari).toBe(true);
|
|
136
|
+
expect(info.isMacSafari).toBe(true);
|
|
137
|
+
expect(info.isIOSSafari).toBe(false);
|
|
138
|
+
expect(info.isMobile).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
});
|