@musecat/functionkit 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/references/hooks/useIntersectionObserver.md +4 -0
- package/.agents/references/hooks/useInterval.md +4 -0
- package/.agents/references/hooks/usePreservedCallback.md +4 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/.github/workflows/ci.yml +20 -0
- package/.local/state/gh/device-id +1 -0
- package/.prettierrc +8 -0
- package/AGENTS.md +14 -0
- package/CODE_OF_CONDUCT.md +55 -0
- package/CONTRIBUTING.md +39 -0
- package/biome.json +25 -0
- package/index.ts +1 -1
- package/package.json +21 -2
- package/packages/components/ScrolltoTop.tsx +1 -1
- package/packages/components/ViewportPortal.tsx +1 -2
- package/packages/cookie/cookie.shared.ts +7 -33
- package/packages/cookie/cookieNames.shared.ts +9 -0
- package/packages/datetime/dateTime.client.ts +1 -3
- package/packages/datetime/dateTime.server.ts +1 -3
- package/packages/datetime/dateTime.shared.ts +9 -47
- package/packages/hooks/useCheckInvisible.ts +1 -1
- package/packages/hooks/useClientDateTime.ts +3 -16
- package/packages/hooks/useDebounce.ts +2 -7
- package/packages/hooks/useGeolocation.ts +1 -5
- package/packages/hooks/useInterval.ts +1 -2
- package/packages/hooks/useKeyboardListNavigation.ts +10 -38
- package/packages/hooks/useLongPress.ts +13 -24
- package/packages/hooks/useRelativeDateTime.ts +1 -4
- package/packages/hooks/useViewportHeight.ts +1 -2
- package/packages/hooks/useViewportMatch.ts +0 -4
- package/packages/utils/buildContext.tsx +2 -3
- package/packages/utils/floatingMotion.ts +14 -20
- package/packages/utils/subscribeKeyboardHeight.ts +3 -0
- package/tests/components/ScrolltoTop.test.tsx +1 -1
- package/tests/components/SwitchCase.test.tsx +1 -2
- package/tests/components/ViewportPortal.test.tsx +10 -6
- package/tests/cookie/cookie.test.ts +47 -11
- package/tests/datetime/datetime.test.ts +257 -46
- package/tests/hooks/useAvoidKeyboard.test.ts +32 -1
- package/tests/hooks/useCheckInvisible.test.ts +36 -5
- package/tests/hooks/useCheckScroll.test.ts +2 -2
- package/tests/hooks/useClientDateTime.test.ts +2 -4
- package/tests/hooks/useDebounce.test.ts +30 -5
- package/tests/hooks/useDebouncedCallback.test.ts +41 -12
- package/tests/hooks/useDoubleClick.test.ts +58 -7
- package/tests/hooks/useGeolocation.test.ts +155 -4
- package/tests/hooks/useHasMounted.test.ts +1 -1
- package/tests/hooks/useIntersectionObserver.test.ts +70 -5
- package/tests/hooks/useInterval.test.ts +28 -3
- package/tests/hooks/useKeyboardHeight.test.ts +1 -1
- package/tests/hooks/useKeyboardListNavigation.test.ts +340 -18
- package/tests/hooks/useLongPress.test.ts +177 -15
- package/tests/hooks/usePreservedCallback.test.ts +7 -9
- package/tests/hooks/usePreservedReference.test.ts +19 -12
- package/tests/hooks/useRefEffect.test.ts +43 -6
- package/tests/hooks/useRelativeDateTime.test.ts +8 -6
- package/tests/hooks/useTimeout.test.ts +43 -5
- package/tests/hooks/useToggleState.test.ts +14 -6
- package/tests/hooks/useViewportHeight.test.ts +24 -2
- package/tests/hooks/useViewportMatch.test.ts +1 -1
- package/tests/utils/browserStorage.test.ts +68 -8
- package/tests/utils/buildContext.test.tsx +1 -2
- package/tests/utils/checkDevice.test.ts +62 -6
- package/tests/utils/clipboardShare.test.ts +79 -5
- package/tests/utils/floatingMotion.test.ts +60 -9
- package/tests/utils/keyboardTarget.test.ts +42 -1
- package/tests/utils/mergeRefs.test.ts +33 -1
- package/tests/utils/seen.test.ts +16 -7
- package/tests/utils/subscribeKeyboardHeight.test.ts +124 -2
- package/vitest.config.ts +7 -1
|
@@ -1,35 +1,42 @@
|
|
|
1
|
-
import { describe, test, expect, vi } from "vitest";
|
|
2
1
|
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
3
|
import { usePreservedReference } from "@/packages/hooks/usePreservedReference";
|
|
4
4
|
|
|
5
5
|
describe("usePreservedReference", () => {
|
|
6
6
|
test("returns same reference for deeply equal values", () => {
|
|
7
|
-
const { result, rerender } = renderHook(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
);
|
|
7
|
+
const { result, rerender } = renderHook(({ val }) => usePreservedReference(val), {
|
|
8
|
+
initialProps: { val: { a: 1, b: { c: [2, 3] } } },
|
|
9
|
+
});
|
|
11
10
|
const first = result.current;
|
|
12
11
|
rerender({ val: { a: 1, b: { c: [2, 3] } } });
|
|
13
12
|
expect(result.current).toBe(first);
|
|
14
13
|
});
|
|
15
14
|
|
|
16
15
|
test("returns new reference for different values", () => {
|
|
17
|
-
const { result, rerender } = renderHook(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
);
|
|
16
|
+
const { result, rerender } = renderHook(({ val }) => usePreservedReference(val), {
|
|
17
|
+
initialProps: { val: { a: 1 } },
|
|
18
|
+
});
|
|
21
19
|
const first = result.current;
|
|
22
20
|
rerender({ val: { a: 2 } });
|
|
23
21
|
expect(result.current).not.toBe(first);
|
|
24
22
|
});
|
|
25
23
|
|
|
26
24
|
test("accepts custom comparator", () => {
|
|
25
|
+
const { result, rerender } = renderHook(({ val }) => usePreservedReference(val, () => true), {
|
|
26
|
+
initialProps: { val: { a: 1 } },
|
|
27
|
+
});
|
|
28
|
+
const first = result.current;
|
|
29
|
+
rerender({ val: { a: 999 } });
|
|
30
|
+
expect(result.current).toBe(first);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("custom comparator returning false forces new reference", () => {
|
|
27
34
|
const { result, rerender } = renderHook(
|
|
28
|
-
({ val }) => usePreservedReference(val, () =>
|
|
35
|
+
({ val }) => usePreservedReference(val, () => false),
|
|
29
36
|
{ initialProps: { val: { a: 1 } } },
|
|
30
37
|
);
|
|
31
38
|
const first = result.current;
|
|
32
|
-
rerender({ val: { a:
|
|
33
|
-
expect(result.current).toBe(first);
|
|
39
|
+
rerender({ val: { a: 1 } });
|
|
40
|
+
expect(result.current).not.toBe(first);
|
|
34
41
|
});
|
|
35
42
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test, vi } from "vitest";
|
|
3
3
|
import { useRefEffect } from "@/packages/hooks/useRefEffect";
|
|
4
4
|
|
|
5
5
|
describe("useRefEffect", () => {
|
|
@@ -9,13 +9,46 @@ describe("useRefEffect", () => {
|
|
|
9
9
|
const { result } = renderHook(() => useRefEffect(callback, []));
|
|
10
10
|
|
|
11
11
|
const element = document.createElement("div");
|
|
12
|
-
act(() => {
|
|
12
|
+
act(() => {
|
|
13
|
+
(result.current as (el: HTMLElement | null) => void)(element);
|
|
14
|
+
});
|
|
13
15
|
expect(callback).toHaveBeenCalledWith(element);
|
|
14
16
|
|
|
15
|
-
act(() => {
|
|
17
|
+
act(() => {
|
|
18
|
+
(result.current as (el: HTMLElement | null) => void)(null);
|
|
19
|
+
});
|
|
16
20
|
expect(cleanup).toHaveBeenCalledOnce();
|
|
17
21
|
});
|
|
18
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
|
+
|
|
19
52
|
test("calls cleanup before setting new element", () => {
|
|
20
53
|
const cleanup = vi.fn();
|
|
21
54
|
const callback = vi.fn(() => cleanup);
|
|
@@ -23,8 +56,12 @@ describe("useRefEffect", () => {
|
|
|
23
56
|
|
|
24
57
|
const el1 = document.createElement("div");
|
|
25
58
|
const el2 = document.createElement("span");
|
|
26
|
-
act(() => {
|
|
27
|
-
|
|
59
|
+
act(() => {
|
|
60
|
+
(result.current as (el: HTMLElement | null) => void)(el1);
|
|
61
|
+
});
|
|
62
|
+
act(() => {
|
|
63
|
+
(result.current as (el: HTMLElement | null) => void)(el2);
|
|
64
|
+
});
|
|
28
65
|
expect(cleanup).toHaveBeenCalledOnce();
|
|
29
66
|
});
|
|
30
67
|
});
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
1
|
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
3
|
import { useRelativeDateTime } from "@/packages/hooks/useRelativeDateTime";
|
|
4
4
|
|
|
5
5
|
describe("useRelativeDateTime", () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
8
12
|
|
|
9
13
|
test("returns relative time text", () => {
|
|
10
14
|
const now = Date.now();
|
|
11
15
|
const fiveMinAgo = new Date(now - 5 * 60 * 1000).toISOString();
|
|
12
|
-
const { result } = renderHook(() =>
|
|
13
|
-
useRelativeDateTime(fiveMinAgo, { locale: "ko" }),
|
|
14
|
-
);
|
|
16
|
+
const { result } = renderHook(() => useRelativeDateTime(fiveMinAgo, { locale: "ko" }));
|
|
15
17
|
expect(result.current.ready).toBe(true);
|
|
16
18
|
expect(result.current.isRelative).toBe(true);
|
|
17
19
|
expect(result.current.text).toBeTruthy();
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
3
|
import { useTimeout } from "@/packages/hooks/useTimeout";
|
|
4
4
|
|
|
5
5
|
describe("useTimeout", () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
8
12
|
|
|
9
13
|
test("calls callback after delay", () => {
|
|
10
14
|
const fn = vi.fn();
|
|
@@ -21,14 +25,48 @@ describe("useTimeout", () => {
|
|
|
21
25
|
expect(fn).not.toHaveBeenCalled();
|
|
22
26
|
});
|
|
23
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
|
+
|
|
24
39
|
test("start and clear controls", () => {
|
|
25
40
|
const fn = vi.fn();
|
|
26
41
|
const { result } = renderHook(() => useTimeout(fn, 1000, { enabled: false }));
|
|
27
42
|
expect(result.current.isPending()).toBe(false);
|
|
28
|
-
act(() => {
|
|
43
|
+
act(() => {
|
|
44
|
+
result.current.start(50);
|
|
45
|
+
});
|
|
29
46
|
expect(result.current.isPending()).toBe(true);
|
|
30
47
|
vi.advanceTimersByTime(50);
|
|
31
48
|
expect(fn).toHaveBeenCalledOnce();
|
|
32
49
|
expect(result.current.isPending()).toBe(false);
|
|
33
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
|
+
});
|
|
34
72
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
3
|
import { useToggleState } from "@/packages/hooks/useToggleState";
|
|
4
4
|
|
|
5
5
|
describe("useToggleState", () => {
|
|
@@ -15,21 +15,29 @@ describe("useToggleState", () => {
|
|
|
15
15
|
|
|
16
16
|
test("setTrue sets value to true", () => {
|
|
17
17
|
const { result } = renderHook(() => useToggleState(false));
|
|
18
|
-
act(() => {
|
|
18
|
+
act(() => {
|
|
19
|
+
result.current.setTrue();
|
|
20
|
+
});
|
|
19
21
|
expect(result.current.value).toBe(true);
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
test("setFalse sets value to false", () => {
|
|
23
25
|
const { result } = renderHook(() => useToggleState(true));
|
|
24
|
-
act(() => {
|
|
26
|
+
act(() => {
|
|
27
|
+
result.current.setFalse();
|
|
28
|
+
});
|
|
25
29
|
expect(result.current.value).toBe(false);
|
|
26
30
|
});
|
|
27
31
|
|
|
28
32
|
test("toggle switches value", () => {
|
|
29
33
|
const { result } = renderHook(() => useToggleState(false));
|
|
30
|
-
act(() => {
|
|
34
|
+
act(() => {
|
|
35
|
+
result.current.toggle();
|
|
36
|
+
});
|
|
31
37
|
expect(result.current.value).toBe(true);
|
|
32
|
-
act(() => {
|
|
38
|
+
act(() => {
|
|
39
|
+
result.current.toggle();
|
|
40
|
+
});
|
|
33
41
|
expect(result.current.value).toBe(false);
|
|
34
42
|
});
|
|
35
43
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test, vi } from "vitest";
|
|
3
3
|
import { useViewportHeight } from "@/packages/hooks/useViewportHeight";
|
|
4
4
|
|
|
5
5
|
describe("useViewportHeight", () => {
|
|
@@ -7,4 +7,26 @@ describe("useViewportHeight", () => {
|
|
|
7
7
|
const { result } = renderHook(() => useViewportHeight());
|
|
8
8
|
expect(result.current.height).toBeGreaterThanOrEqual(0);
|
|
9
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
|
+
});
|
|
10
32
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
2
1
|
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
3
|
import { useViewportMatch } from "@/packages/hooks/useViewportMatch";
|
|
4
4
|
|
|
5
5
|
describe("useViewportMatch", () => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
getLocalStorage,
|
|
4
|
-
updateLocalStorage,
|
|
5
|
-
removeLocalStorage,
|
|
6
4
|
getSessionStorage,
|
|
7
|
-
|
|
5
|
+
removeLocalStorage,
|
|
8
6
|
removeSessionStorage,
|
|
7
|
+
updateLocalStorage,
|
|
8
|
+
updateSessionStorage,
|
|
9
9
|
} from "@/packages/utils/browserStorage";
|
|
10
10
|
|
|
11
11
|
describe("browserStorage", () => {
|
|
@@ -18,16 +18,24 @@ describe("browserStorage", () => {
|
|
|
18
18
|
|
|
19
19
|
const createMockStorage = (store: Record<string, string>) => ({
|
|
20
20
|
getItem: vi.fn((key) => store[key] ?? null),
|
|
21
|
-
setItem: vi.fn((key, value) => {
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}),
|
|
24
30
|
});
|
|
25
31
|
|
|
26
32
|
vi.stubGlobal("localStorage", createMockStorage(mockStorage));
|
|
27
33
|
vi.stubGlobal("sessionStorage", createMockStorage(mockStorage2));
|
|
28
34
|
});
|
|
29
35
|
|
|
30
|
-
afterEach(() => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.unstubAllGlobals();
|
|
38
|
+
});
|
|
31
39
|
|
|
32
40
|
test("localStorage operations", () => {
|
|
33
41
|
updateLocalStorage("foo", { bar: "baz" });
|
|
@@ -51,4 +59,56 @@ describe("browserStorage", () => {
|
|
|
51
59
|
localStorage.setItem("invalid", "not-json");
|
|
52
60
|
expect(getLocalStorage("invalid")).toBeNull();
|
|
53
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
|
+
});
|
|
54
114
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { describe, test, expect } from "vitest";
|
|
2
1
|
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
4
3
|
import { buildContext } from "@/packages/utils/buildContext";
|
|
5
4
|
|
|
6
5
|
describe("buildContext", () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
2
2
|
import { getDeviceInfo } from "@/packages/utils/checkDevice";
|
|
3
3
|
|
|
4
4
|
describe("getDeviceInfo", () => {
|
|
@@ -23,7 +23,8 @@ describe("getDeviceInfo", () => {
|
|
|
23
23
|
|
|
24
24
|
test("detects mobile Chrome", () => {
|
|
25
25
|
Object.defineProperty(navigator, "userAgent", {
|
|
26
|
-
value:
|
|
26
|
+
value:
|
|
27
|
+
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
|
|
27
28
|
writable: true,
|
|
28
29
|
configurable: true,
|
|
29
30
|
});
|
|
@@ -35,7 +36,8 @@ describe("getDeviceInfo", () => {
|
|
|
35
36
|
|
|
36
37
|
test("detects iOS Safari", () => {
|
|
37
38
|
Object.defineProperty(navigator, "userAgent", {
|
|
38
|
-
value:
|
|
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",
|
|
39
41
|
writable: true,
|
|
40
42
|
configurable: true,
|
|
41
43
|
});
|
|
@@ -49,7 +51,8 @@ describe("getDeviceInfo", () => {
|
|
|
49
51
|
|
|
50
52
|
test("detects desktop Chrome", () => {
|
|
51
53
|
Object.defineProperty(navigator, "userAgent", {
|
|
52
|
-
value:
|
|
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",
|
|
53
56
|
writable: true,
|
|
54
57
|
configurable: true,
|
|
55
58
|
});
|
|
@@ -60,7 +63,8 @@ describe("getDeviceInfo", () => {
|
|
|
60
63
|
|
|
61
64
|
test("detects Mac Safari", () => {
|
|
62
65
|
Object.defineProperty(navigator, "userAgent", {
|
|
63
|
-
value:
|
|
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",
|
|
64
68
|
writable: true,
|
|
65
69
|
configurable: true,
|
|
66
70
|
});
|
|
@@ -71,9 +75,41 @@ describe("getDeviceInfo", () => {
|
|
|
71
75
|
expect(info.browser).toBe("safari");
|
|
72
76
|
});
|
|
73
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
|
+
|
|
74
109
|
test("detects Samsung Browser", () => {
|
|
75
110
|
Object.defineProperty(navigator, "userAgent", {
|
|
76
|
-
value:
|
|
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",
|
|
77
113
|
writable: true,
|
|
78
114
|
configurable: true,
|
|
79
115
|
});
|
|
@@ -81,4 +117,24 @@ describe("getDeviceInfo", () => {
|
|
|
81
117
|
expect(info.isSamsungBrowser).toBe(true);
|
|
82
118
|
expect(info.browser).toBe("samsung");
|
|
83
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
|
+
});
|
|
84
140
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import { NavigatorClipboard, NavigatorShare } from "@/packages/utils/clipboardShare";
|
|
3
3
|
|
|
4
4
|
describe("NavigatorClipboard", () => {
|
|
@@ -26,7 +26,9 @@ describe("NavigatorClipboard", () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
describe("NavigatorShare", () => {
|
|
29
|
-
afterEach(() => {
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
30
32
|
|
|
31
33
|
test("uses Web Share API when available", async () => {
|
|
32
34
|
Object.defineProperty(navigator, "share", {
|
|
@@ -40,7 +42,11 @@ describe("NavigatorShare", () => {
|
|
|
40
42
|
configurable: true,
|
|
41
43
|
});
|
|
42
44
|
|
|
43
|
-
const result = await NavigatorShare({
|
|
45
|
+
const result = await NavigatorShare({
|
|
46
|
+
title: "Test",
|
|
47
|
+
text: "Hello",
|
|
48
|
+
link: "https://example.com",
|
|
49
|
+
});
|
|
44
50
|
expect(result).toEqual({ success: true, method: "share" });
|
|
45
51
|
});
|
|
46
52
|
|
|
@@ -56,10 +62,74 @@ describe("NavigatorShare", () => {
|
|
|
56
62
|
configurable: true,
|
|
57
63
|
});
|
|
58
64
|
|
|
59
|
-
const result = await NavigatorShare({
|
|
65
|
+
const result = await NavigatorShare({
|
|
66
|
+
title: "Test",
|
|
67
|
+
text: "Hello",
|
|
68
|
+
link: "https://example.com",
|
|
69
|
+
});
|
|
70
|
+
expect(result).toEqual({ success: true, method: "clipboard" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("handles AbortError from Web Share API", async () => {
|
|
74
|
+
Object.defineProperty(navigator, "share", {
|
|
75
|
+
value: vi.fn(() => Promise.reject(new DOMException("Aborted", "AbortError"))),
|
|
76
|
+
writable: true,
|
|
77
|
+
configurable: true,
|
|
78
|
+
});
|
|
79
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
80
|
+
value: { writeText: vi.fn() },
|
|
81
|
+
writable: true,
|
|
82
|
+
configurable: true,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = await NavigatorShare({
|
|
86
|
+
title: "Test",
|
|
87
|
+
text: "Hello",
|
|
88
|
+
link: "https://example.com",
|
|
89
|
+
});
|
|
90
|
+
expect(result).toEqual({ success: false, method: "share" });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("falls back to clipboard on non-AbortError share rejection", async () => {
|
|
94
|
+
Object.defineProperty(navigator, "share", {
|
|
95
|
+
value: vi.fn(() => Promise.reject(new Error("NotAllowedError"))),
|
|
96
|
+
writable: true,
|
|
97
|
+
configurable: true,
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
100
|
+
value: { writeText: vi.fn(() => Promise.resolve()) },
|
|
101
|
+
writable: true,
|
|
102
|
+
configurable: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await NavigatorShare({
|
|
106
|
+
title: "Test",
|
|
107
|
+
text: "Hello",
|
|
108
|
+
link: "https://example.com",
|
|
109
|
+
});
|
|
60
110
|
expect(result).toEqual({ success: true, method: "clipboard" });
|
|
61
111
|
});
|
|
62
112
|
|
|
113
|
+
test("clipboard fallback rejects with unsupported", async () => {
|
|
114
|
+
Object.defineProperty(navigator, "share", {
|
|
115
|
+
value: vi.fn(() => Promise.reject(new Error("share failed"))),
|
|
116
|
+
writable: true,
|
|
117
|
+
configurable: true,
|
|
118
|
+
});
|
|
119
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
120
|
+
value: { writeText: vi.fn(() => Promise.reject(new Error("clipboard denied"))) },
|
|
121
|
+
writable: true,
|
|
122
|
+
configurable: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = await NavigatorShare({
|
|
126
|
+
title: "Test",
|
|
127
|
+
text: "Hello",
|
|
128
|
+
link: "https://example.com",
|
|
129
|
+
});
|
|
130
|
+
expect(result).toEqual({ success: false, method: "unsupported" });
|
|
131
|
+
});
|
|
132
|
+
|
|
63
133
|
test("returns unsupported when neither share nor clipboard", async () => {
|
|
64
134
|
Object.defineProperty(navigator, "share", {
|
|
65
135
|
value: undefined,
|
|
@@ -72,7 +142,11 @@ describe("NavigatorShare", () => {
|
|
|
72
142
|
configurable: true,
|
|
73
143
|
});
|
|
74
144
|
|
|
75
|
-
const result = await NavigatorShare({
|
|
145
|
+
const result = await NavigatorShare({
|
|
146
|
+
title: "Test",
|
|
147
|
+
text: "Hello",
|
|
148
|
+
link: "https://example.com",
|
|
149
|
+
});
|
|
76
150
|
expect(result).toEqual({ success: false, method: "unsupported" });
|
|
77
151
|
});
|
|
78
152
|
});
|