@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
@@ -1,99 +1,99 @@
1
1
  export type FloatingMotionMode =
2
- | "anchored"
3
- | "center-selected"
4
- | "modal-center"
5
- | "mobile-sheet";
2
+ | "anchored"
3
+ | "center-selected"
4
+ | "modal-center"
5
+ | "mobile-sheet";
6
6
 
7
7
  export type FloatingPlacement =
8
- | "top-left"
9
- | "top-center"
10
- | "top-right"
11
- | "middle-left"
12
- | "middle-center"
13
- | "middle-right"
14
- | "bottom-left"
15
- | "bottom-center"
16
- | "bottom-right";
8
+ | "top-left"
9
+ | "top-center"
10
+ | "top-right"
11
+ | "middle-left"
12
+ | "middle-center"
13
+ | "middle-right"
14
+ | "bottom-left"
15
+ | "bottom-center"
16
+ | "bottom-right";
17
17
 
18
18
  export interface FloatingMotionPreset {
19
- enterMs: number;
20
- exitMs: number;
21
- ease: string;
19
+ enterMs: number;
20
+ exitMs: number;
21
+ ease: string;
22
22
  }
23
23
 
24
24
  export const getFloatingMotionPreset = (
25
- mode: FloatingMotionMode,
25
+ mode: FloatingMotionMode,
26
26
  ): FloatingMotionPreset => {
27
- if (mode === "mobile-sheet") {
28
- return {
29
- enterMs: 360,
30
- exitMs: 280,
31
- ease: "cubic-bezier(0.2, 0.8, 0.2, 1)",
32
- };
33
- }
27
+ if (mode === "mobile-sheet") {
28
+ return {
29
+ enterMs: 360,
30
+ exitMs: 280,
31
+ ease: "cubic-bezier(0.2, 0.8, 0.2, 1)",
32
+ };
33
+ }
34
34
 
35
- if (mode === "modal-center") {
36
- return {
37
- enterMs: 340,
38
- exitMs: 260,
39
- ease: "cubic-bezier(0.22, 1, 0.36, 1)",
40
- };
41
- }
35
+ if (mode === "modal-center") {
36
+ return {
37
+ enterMs: 340,
38
+ exitMs: 260,
39
+ ease: "cubic-bezier(0.22, 1, 0.36, 1)",
40
+ };
41
+ }
42
42
 
43
- if (mode === "center-selected") {
44
- return {
45
- enterMs: 190,
46
- exitMs: 140,
47
- ease: "cubic-bezier(0.2, 0, 0, 1)",
48
- };
49
- }
43
+ if (mode === "center-selected") {
44
+ return {
45
+ enterMs: 190,
46
+ exitMs: 140,
47
+ ease: "cubic-bezier(0.2, 0, 0, 1)",
48
+ };
49
+ }
50
50
 
51
- return {
52
- enterMs: 230,
53
- exitMs: 170,
54
- ease: "cubic-bezier(0.16, 1, 0.3, 1)",
55
- };
51
+ return {
52
+ enterMs: 230,
53
+ exitMs: 170,
54
+ ease: "cubic-bezier(0.16, 1, 0.3, 1)",
55
+ };
56
56
  };
57
57
 
58
58
  export const getFloatingTransformOrigin = (
59
- placement?: FloatingPlacement,
59
+ placement?: FloatingPlacement,
60
60
  ): string => {
61
- if (!placement) return "top center";
61
+ if (!placement) return "top center";
62
62
 
63
- const [row, col] = placement.split("-") as [
64
- "top" | "middle" | "bottom",
65
- "left" | "center" | "right",
66
- ];
67
- const y = row === "top" ? "bottom" : row === "bottom" ? "top" : "center";
68
- const x = col === "right" ? "right" : col === "left" ? "left" : "center";
63
+ const [row, col] = placement.split("-") as [
64
+ "top" | "middle" | "bottom",
65
+ "left" | "center" | "right",
66
+ ];
67
+ const y = row === "top" ? "bottom" : row === "bottom" ? "top" : "center";
68
+ const x = col === "right" ? "right" : col === "left" ? "left" : "center";
69
69
 
70
- return `${y} ${x}`;
70
+ return `${y} ${x}`;
71
71
  };
72
72
 
73
73
  export const getFloatingHiddenTransform = ({
74
- mode,
75
- placement,
74
+ mode,
75
+ placement,
76
76
  }: {
77
- mode: FloatingMotionMode;
78
- placement?: FloatingPlacement;
77
+ mode: FloatingMotionMode;
78
+ placement?: FloatingPlacement;
79
79
  }): string => {
80
- if (mode === "mobile-sheet") return "translateY(1.8rem) scale(1)";
81
- if (mode === "modal-center") return "translateY(.8rem) scale(.94)";
82
- if (mode === "center-selected") return "translateY(.45rem) scale(.99)";
83
- if (!placement) return "translateY(.4rem) scale(.975)";
80
+ if (mode === "mobile-sheet") return "translateY(1.8rem) scale(1)";
81
+ if (mode === "modal-center") return "translateY(.8rem) scale(.94)";
82
+ if (mode === "center-selected") return "translateY(.45rem) scale(.99)";
83
+ if (!placement) return "translateY(.4rem) scale(.975)";
84
84
 
85
- const [row, col] = placement.split("-") as [
86
- "top" | "middle" | "bottom",
87
- "left" | "center" | "right",
88
- ];
85
+ const [row, col] = placement.split("-") as [
86
+ "top" | "middle" | "bottom",
87
+ "left" | "center" | "right",
88
+ ];
89
89
 
90
- if (row === "top") return "translateY(.4rem) scale(.975)";
91
- if (row === "bottom") return "translateY(-.4rem) scale(.975)";
90
+ if (row === "top") return "translateY(.4rem) scale(.975)";
91
+ if (row === "bottom") return "translateY(-.4rem) scale(.975)";
92
92
 
93
- if (row === "middle") {
94
- if (col === "left") return "translateX(.4rem) scale(.975)";
95
- if (col === "right") return "translateX(-.4rem) scale(.975)";
96
- }
93
+ if (row === "middle") {
94
+ if (col === "left") return "translateX(.4rem) scale(.975)";
95
+ if (col === "right") return "translateX(-.4rem) scale(.975)";
96
+ }
97
97
 
98
- return "translateY(.4rem) scale(.975)";
98
+ return "translateY(.4rem) scale(.975)";
99
99
  };
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  export function isEditableKeyboardTarget(target: HTMLElement) {
4
- return (
5
- target.isContentEditable ||
6
- target.tagName === "TEXTAREA" ||
7
- target.tagName === "SELECT" ||
8
- (target.tagName === "INPUT" &&
9
- (target as HTMLInputElement).type !== "button" &&
10
- (target as HTMLInputElement).type !== "checkbox" &&
11
- (target as HTMLInputElement).type !== "radio")
12
- );
4
+ return (
5
+ target.isContentEditable ||
6
+ target.tagName === "TEXTAREA" ||
7
+ target.tagName === "SELECT" ||
8
+ (target.tagName === "INPUT" &&
9
+ (target as HTMLInputElement).type !== "button" &&
10
+ (target as HTMLInputElement).type !== "checkbox" &&
11
+ (target as HTMLInputElement).type !== "radio")
12
+ );
13
13
  }
@@ -3,13 +3,13 @@ import type { Ref, RefCallback } from "react";
3
3
  type ReactRef<T> = Ref<T> | undefined | null;
4
4
 
5
5
  export function mergeRefs<T>(...refs: ReactRef<T>[]): RefCallback<T> {
6
- return (value: T | null) => {
7
- for (const ref of refs) {
8
- if (typeof ref === "function") {
9
- ref(value);
10
- } else if (ref && "current" in ref) {
11
- (ref as { current: T | null }).current = value;
12
- }
13
- }
14
- };
6
+ return (value: T | null) => {
7
+ for (const ref of refs) {
8
+ if (typeof ref === "function") {
9
+ ref(value);
10
+ } else if (ref && "current" in ref) {
11
+ (ref as { current: T | null }).current = value;
12
+ }
13
+ }
14
+ };
15
15
  }
@@ -3,27 +3,27 @@ export const SEEN_STORAGE_KEY = "seen";
3
3
  type SeenMap = Partial<Record<string, boolean>>;
4
4
 
5
5
  export function parseSeen(raw: string | null): SeenMap {
6
- if (!raw) return {};
6
+ if (!raw) return {};
7
7
 
8
- try {
9
- const parsed = JSON.parse(raw) as unknown;
10
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
11
- return {};
12
- }
8
+ try {
9
+ const parsed = JSON.parse(raw) as unknown;
10
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
11
+ return {};
12
+ }
13
13
 
14
- return parsed as SeenMap;
15
- } catch {
16
- return {};
17
- }
14
+ return parsed as SeenMap;
15
+ } catch {
16
+ return {};
17
+ }
18
18
  }
19
19
 
20
20
  export function hasSeenKey(raw: string | null, key: string) {
21
- const map = parseSeen(raw);
22
- return map[key] === true;
21
+ const map = parseSeen(raw);
22
+ return map[key] === true;
23
23
  }
24
24
 
25
25
  export function buildSeenValue(raw: string | null, key: string): string {
26
- const map = parseSeen(raw);
27
- map[key] = true;
28
- return JSON.stringify(map);
26
+ const map = parseSeen(raw);
27
+ map[key] = true;
28
+ return JSON.stringify(map);
29
29
  }
@@ -1,54 +1,54 @@
1
1
  "use client";
2
2
 
3
3
  type SubscribeKeyboardHeightOptions = {
4
- callback: (height: number) => void;
5
- immediate?: boolean;
6
- throttleMs?: number;
4
+ callback: (height: number) => void;
5
+ immediate?: boolean;
6
+ throttleMs?: number;
7
7
  };
8
8
 
9
9
  export function subscribeKeyboardHeight({
10
- callback,
11
- immediate = false,
12
- throttleMs = 16,
10
+ callback,
11
+ immediate = false,
12
+ throttleMs = 16,
13
13
  }: SubscribeKeyboardHeightOptions) {
14
- let ticking = false;
15
- let lastHeight = 0;
16
-
17
- const notify = (height: number) => {
18
- if (ticking) return;
19
- ticking = true;
20
-
21
- const run = () => {
22
- if (height !== lastHeight) {
23
- lastHeight = height;
24
- callback(height);
25
- }
26
- ticking = false;
27
- };
28
-
29
- if (throttleMs > 0) {
30
- setTimeout(run, throttleMs);
31
- } else {
32
- run();
33
- }
34
- };
35
-
36
- const handleResize = () => {
37
- const vv = window.visualViewport;
38
- if (!vv) return;
39
- const diff = window.innerHeight - vv.height;
40
- notify(diff > 0 ? diff : 0);
41
- };
42
-
43
- if (immediate) handleResize();
44
-
45
- window.visualViewport?.addEventListener("resize", handleResize);
46
- window.visualViewport?.addEventListener("scroll", handleResize);
47
-
48
- return {
49
- unsubscribe() {
50
- window.visualViewport?.removeEventListener("resize", handleResize);
51
- window.visualViewport?.removeEventListener("scroll", handleResize);
52
- },
53
- };
14
+ let ticking = false;
15
+ let lastHeight = 0;
16
+
17
+ const notify = (height: number) => {
18
+ if (ticking) return;
19
+ ticking = true;
20
+
21
+ const run = () => {
22
+ if (height !== lastHeight) {
23
+ lastHeight = height;
24
+ callback(height);
25
+ }
26
+ ticking = false;
27
+ };
28
+
29
+ if (throttleMs > 0) {
30
+ setTimeout(run, throttleMs);
31
+ } else {
32
+ run();
33
+ }
34
+ };
35
+
36
+ const handleResize = () => {
37
+ const vv = window.visualViewport;
38
+ if (!vv) return;
39
+ const diff = window.innerHeight - vv.height;
40
+ notify(diff > 0 ? diff : 0);
41
+ };
42
+
43
+ if (immediate) handleResize();
44
+
45
+ window.visualViewport?.addEventListener("resize", handleResize);
46
+ window.visualViewport?.addEventListener("scroll", handleResize);
47
+
48
+ return {
49
+ unsubscribe() {
50
+ window.visualViewport?.removeEventListener("resize", handleResize);
51
+ window.visualViewport?.removeEventListener("scroll", handleResize);
52
+ },
53
+ };
54
54
  }
@@ -0,0 +1,15 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { ScrolltoTop } from "@/packages/components/ScrolltoTop";
4
+
5
+ describe("ScrolltoTop", () => {
6
+ test("calls window.scrollTo(0, 0) on mount", () => {
7
+ const originalScrollTo = window.scrollTo;
8
+ window.scrollTo = vi.fn();
9
+
10
+ render(<ScrolltoTop />);
11
+ expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
12
+
13
+ window.scrollTo = originalScrollTo;
14
+ });
15
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { SwitchCase } from "@/packages/components/SwitchCase";
4
+ import React from "react";
5
+
6
+ describe("SwitchCase", () => {
7
+ const cases = {
8
+ 1: <div>Case 1</div>,
9
+ 2: <div>Case 2</div>,
10
+ three: <div>Case Three</div>,
11
+ };
12
+
13
+ test("renders correctly matching case", () => {
14
+ const { container } = render(<SwitchCase value={1} cases={cases} />);
15
+ expect(container.textContent).toBe("Case 1");
16
+ });
17
+
18
+ test("renders another case", () => {
19
+ const { container } = render(<SwitchCase value="three" cases={cases} />);
20
+ expect(container.textContent).toBe("Case Three");
21
+ });
22
+
23
+ test("renders otherwise when case does not exist", () => {
24
+ const { container } = render(
25
+ <SwitchCase value={99} cases={cases} otherwise={<div>Fallback</div>} />,
26
+ );
27
+ expect(container.textContent).toBe("Fallback");
28
+ });
29
+
30
+ test("renders null when case does not exist and otherwise is not provided", () => {
31
+ const { container } = render(<SwitchCase value={99} cases={cases} />);
32
+ expect(container.innerHTML).toBe("");
33
+ });
34
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, test, expect, beforeEach } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import {
4
+ ViewportPortal,
5
+ getViewportPortalRoot,
6
+ } from "@/packages/components/ViewportPortal";
7
+ import React from "react";
8
+
9
+ describe("ViewportPortal", () => {
10
+ beforeEach(() => {
11
+ document.body.innerHTML = "";
12
+ });
13
+
14
+ test("getViewportPortalRoot creates a root element if it doesn't exist", () => {
15
+ const root = getViewportPortalRoot();
16
+ expect(root).not.toBeNull();
17
+ expect(root?.id).toBe("viewport-portal-root");
18
+ expect(document.getElementById("viewport-portal-root")).toBe(root);
19
+ });
20
+
21
+ test("getViewportPortalRoot returns existing root element if it exists", () => {
22
+ const root1 = getViewportPortalRoot();
23
+ const root2 = getViewportPortalRoot();
24
+ expect(root1).toBe(root2);
25
+ expect(document.querySelectorAll("#viewport-portal-root").length).toBe(1);
26
+ });
27
+
28
+ test("ViewportPortal renders children into the portal root", () => {
29
+ render(
30
+ <ViewportPortal>
31
+ <div data-testid="portal-content">Portal Content</div>
32
+ </ViewportPortal>,
33
+ );
34
+
35
+ const root = document.getElementById("viewport-portal-root");
36
+ expect(root).not.toBeNull();
37
+
38
+ const content = screen.getByTestId("portal-content");
39
+ expect(content.textContent).toBe("Portal Content");
40
+ expect(root?.contains(content)).toBe(true);
41
+ });
42
+ });
@@ -0,0 +1,156 @@
1
+ import { describe, test, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import {
3
+ parseClientCookieNames,
4
+ getClientCookie,
5
+ setClientCookie,
6
+ clearClientCookie,
7
+ clearAllClientCookies,
8
+ } from "@/packages/cookie/cookie.shared";
9
+
10
+ describe("cookie module", () => {
11
+ beforeEach(() => {
12
+ // Reset cookies
13
+ Object.defineProperty(document, "cookie", {
14
+ writable: true,
15
+ value: "",
16
+ });
17
+
18
+ // Mock window location
19
+ Object.defineProperty(window, "location", {
20
+ writable: true,
21
+ value: { hostname: "example.com", pathname: "/test/path" },
22
+ });
23
+ });
24
+
25
+ afterEach(() => {
26
+ vi.restoreAllMocks();
27
+ });
28
+
29
+ describe("parseClientCookieNames", () => {
30
+ test("parses cookie string correctly", () => {
31
+ const result = parseClientCookieNames("foo=bar; baz=qux; ; empty=");
32
+ expect(result).toEqual(["foo", "baz", "empty"]);
33
+ });
34
+
35
+ test("returns empty array for empty string", () => {
36
+ expect(parseClientCookieNames("")).toEqual([]);
37
+ });
38
+ });
39
+
40
+ describe("getClientCookie", () => {
41
+ test("returns cookie value if it exists", () => {
42
+ document.cookie = "foo=bar; baz=qux";
43
+ expect(getClientCookie("foo")).toBe("bar");
44
+ expect(getClientCookie("baz")).toBe("qux");
45
+ });
46
+
47
+ test("returns undefined if cookie does not exist", () => {
48
+ document.cookie = "foo=bar";
49
+ expect(getClientCookie("missing")).toBeUndefined();
50
+ });
51
+
52
+ test("handles undefined document", () => {
53
+ const originalDocument = global.document;
54
+ // @ts-ignore
55
+ delete global.document;
56
+
57
+ expect(getClientCookie("foo")).toBeUndefined();
58
+
59
+ global.document = originalDocument;
60
+ });
61
+ });
62
+
63
+ describe("setClientCookie", () => {
64
+ test("sets cookie with name and value", () => {
65
+ setClientCookie("foo", "bar");
66
+ expect(document.cookie).toBe("foo=bar; path=/");
67
+ });
68
+
69
+ test("sets cookie with days", () => {
70
+ vi.useFakeTimers();
71
+ vi.setSystemTime(new Date("2020-01-01T00:00:00Z"));
72
+
73
+ setClientCookie("foo", "bar", 7);
74
+
75
+ const expectedDate = new Date("2020-01-08T00:00:00Z").toUTCString();
76
+ expect(document.cookie).toBe(`foo=bar; expires=${expectedDate}; path=/`);
77
+
78
+ vi.useRealTimers();
79
+ });
80
+
81
+ test("encodes cookie value", () => {
82
+ setClientCookie("foo", "val ue;");
83
+ expect(document.cookie).toBe("foo=val%20ue%3B; path=/");
84
+ });
85
+ });
86
+
87
+ describe("clearClientCookie", () => {
88
+ test("clears cookie with default options", () => {
89
+ document.cookie = "foo=bar";
90
+ clearClientCookie("foo");
91
+ expect(document.cookie).toBe(
92
+ "foo=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=example.com;",
93
+ );
94
+ });
95
+
96
+ test("clears cookie with custom options", () => {
97
+ const mockDoc = { cookie: "" };
98
+ clearClientCookie("foo", {
99
+ hostname: "custom.com",
100
+ path: "/custom",
101
+ documentRef: mockDoc as any,
102
+ });
103
+ expect(mockDoc.cookie).toBe(
104
+ "foo=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/custom; domain=custom.com;",
105
+ );
106
+ });
107
+ });
108
+
109
+ describe("clearAllClientCookies", () => {
110
+ test("clears all cookies based on documentRef.cookie", () => {
111
+ const mockDoc = { cookie: "a=1; b=2" };
112
+ const entries = clearAllClientCookies({ documentRef: mockDoc as any });
113
+
114
+ expect(entries).toEqual(["a", "b"]);
115
+ expect(mockDoc.cookie).toContain(
116
+ "b=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/test/path; domain=com;",
117
+ );
118
+ });
119
+
120
+ test("clears cookies using cookieStore if provided", async () => {
121
+ const cookieStore = { delete: vi.fn().mockResolvedValue(undefined) };
122
+ const mockDoc = { cookie: "a=1; b=2" };
123
+
124
+ const entries = clearAllClientCookies({
125
+ documentRef: mockDoc as any,
126
+ cookieStore: cookieStore as any,
127
+ });
128
+
129
+ expect(entries).toEqual(["a", "b"]);
130
+ expect(cookieStore.delete).toHaveBeenCalledWith("a");
131
+ expect(cookieStore.delete).toHaveBeenCalledWith("b");
132
+ });
133
+
134
+ test("clears cookies from root path when includeRoot is true", () => {
135
+ const mockDoc = { cookie: "a=1" };
136
+ clearAllClientCookies({ documentRef: mockDoc as any, includeRoot: true });
137
+
138
+ // The function iterates and updates document.cookie, so it will contain the last assignment.
139
+ // With paths ["/", "/test/path"] and domain "example.com"
140
+ expect(mockDoc.cookie).toContain(
141
+ "a=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/test/path; domain=com;",
142
+ );
143
+ });
144
+
145
+ test("uses provided cookieString instead of documentRef.cookie", () => {
146
+ const mockDoc = { cookie: "a=1" };
147
+ const entries = clearAllClientCookies({
148
+ documentRef: mockDoc as any,
149
+ cookieString: "x=1; y=2",
150
+ });
151
+
152
+ expect(entries).toEqual(["x", "y"]);
153
+ expect(mockDoc.cookie).toContain("y=;");
154
+ });
155
+ });
156
+ });