@pyreon/hooks 0.11.0 → 0.11.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 (64) hide show
  1. package/package.json +14 -12
  2. package/src/__tests__/useBreakpoint.test.ts +148 -0
  3. package/src/__tests__/useClickOutside.test.ts +144 -0
  4. package/src/__tests__/useColorScheme.test.ts +101 -0
  5. package/src/__tests__/useControllableState.test.ts +45 -0
  6. package/src/__tests__/useDebouncedCallback.test.ts +80 -0
  7. package/src/__tests__/useDebouncedValue.test.ts +112 -0
  8. package/src/__tests__/useElementSize.test.ts +190 -0
  9. package/src/__tests__/useFocus.test.ts +52 -0
  10. package/src/__tests__/useFocusTrap.test.ts +162 -0
  11. package/src/__tests__/useHover.test.ts +62 -0
  12. package/src/__tests__/useIntersection.test.ts +154 -0
  13. package/src/__tests__/useInterval.test.ts +48 -0
  14. package/src/__tests__/useIsomorphicLayoutEffect.test.ts +9 -0
  15. package/src/__tests__/useKeyboard.test.ts +144 -0
  16. package/src/__tests__/useLatest.test.ts +24 -0
  17. package/src/__tests__/useMediaQuery.test.ts +143 -0
  18. package/src/__tests__/useMergedRef.test.ts +48 -0
  19. package/src/__tests__/usePrevious.test.ts +70 -0
  20. package/src/__tests__/useReducedMotion.test.ts +96 -0
  21. package/src/__tests__/useRootSize.test.ts +27 -0
  22. package/src/__tests__/useScrollLock.test.ts +123 -0
  23. package/src/__tests__/useSpacing.test.ts +23 -0
  24. package/src/__tests__/useThemeValue.test.ts +12 -0
  25. package/src/__tests__/useThrottledCallback.test.ts +56 -0
  26. package/src/__tests__/useTimeout.test.ts +54 -0
  27. package/src/__tests__/useToggle.test.ts +82 -0
  28. package/src/__tests__/useUpdateEffect.test.ts +48 -0
  29. package/src/__tests__/useWindowResize.test.ts +139 -0
  30. package/src/index.ts +56 -0
  31. package/src/useBreakpoint.ts +52 -0
  32. package/src/useClickOutside.ts +23 -0
  33. package/src/useClipboard.ts +51 -0
  34. package/src/useColorScheme.ts +10 -0
  35. package/src/useControllableState.ts +39 -0
  36. package/src/useDebouncedCallback.ts +57 -0
  37. package/src/useDebouncedValue.ts +24 -0
  38. package/src/useDialog.ts +83 -0
  39. package/src/useElementSize.ts +38 -0
  40. package/src/useEventListener.ts +31 -0
  41. package/src/useFocus.ts +24 -0
  42. package/src/useFocusTrap.ts +42 -0
  43. package/src/useHover.ts +30 -0
  44. package/src/useInfiniteScroll.ts +115 -0
  45. package/src/useIntersection.ts +30 -0
  46. package/src/useInterval.ts +31 -0
  47. package/src/useIsomorphicLayoutEffect.ts +19 -0
  48. package/src/useKeyboard.ts +28 -0
  49. package/src/useLatest.ts +17 -0
  50. package/src/useMediaQuery.ts +27 -0
  51. package/src/useMergedRef.ts +24 -0
  52. package/src/useOnline.ts +31 -0
  53. package/src/usePrevious.ts +18 -0
  54. package/src/useReducedMotion.ts +8 -0
  55. package/src/useRootSize.ts +28 -0
  56. package/src/useScrollLock.ts +37 -0
  57. package/src/useSpacing.ts +26 -0
  58. package/src/useThemeValue.ts +21 -0
  59. package/src/useThrottledCallback.ts +30 -0
  60. package/src/useTimeAgo.ts +134 -0
  61. package/src/useTimeout.ts +42 -0
  62. package/src/useToggle.ts +22 -0
  63. package/src/useUpdateEffect.ts +24 -0
  64. package/src/useWindowResize.ts +39 -0
@@ -0,0 +1,190 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest"
2
+
3
+ let mountCallbacks: Array<() => unknown> = []
4
+ let unmountCallbacks: Array<() => void> = []
5
+
6
+ vi.mock("@pyreon/core", () => ({
7
+ onMount: (fn: () => unknown) => {
8
+ mountCallbacks.push(fn)
9
+ },
10
+ onUnmount: (fn: () => void) => {
11
+ unmountCallbacks.push(fn)
12
+ },
13
+ }))
14
+
15
+ import { useElementSize } from "../useElementSize"
16
+
17
+ describe("useElementSize", () => {
18
+ let resizeCallback: ((entries: ResizeObserverEntry[]) => void) | undefined
19
+ let observeSpy: ReturnType<typeof vi.fn>
20
+ let disconnectSpy: ReturnType<typeof vi.fn>
21
+
22
+ beforeEach(() => {
23
+ mountCallbacks = []
24
+ unmountCallbacks = []
25
+ resizeCallback = undefined
26
+ observeSpy = vi.fn()
27
+ disconnectSpy = vi.fn()
28
+
29
+ globalThis.ResizeObserver = vi.fn(function (this: unknown, cb: ResizeObserverCallback) {
30
+ resizeCallback = cb as (entries: ResizeObserverEntry[]) => void
31
+ return {
32
+ observe: observeSpy,
33
+ unobserve: vi.fn(),
34
+ disconnect: disconnectSpy,
35
+ }
36
+ }) as unknown as typeof ResizeObserver
37
+ })
38
+
39
+ it("returns initial size of 0x0", () => {
40
+ const size = useElementSize(() => null)
41
+ expect(size().width).toBe(0)
42
+ expect(size().height).toBe(0)
43
+ })
44
+
45
+ it("measures initial element size on mount", () => {
46
+ const el = document.createElement("div")
47
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue({
48
+ width: 200,
49
+ height: 100,
50
+ x: 0,
51
+ y: 0,
52
+ top: 0,
53
+ left: 0,
54
+ bottom: 100,
55
+ right: 200,
56
+ toJSON: () => {
57
+ /* no-op */
58
+ },
59
+ })
60
+
61
+ const size = useElementSize(() => el)
62
+ mountCallbacks.forEach((cb) => {
63
+ cb()
64
+ })
65
+
66
+ expect(size().width).toBe(200)
67
+ expect(size().height).toBe(100)
68
+ })
69
+
70
+ it("observes the element with ResizeObserver", () => {
71
+ const el = document.createElement("div")
72
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue({
73
+ width: 0,
74
+ height: 0,
75
+ x: 0,
76
+ y: 0,
77
+ top: 0,
78
+ left: 0,
79
+ bottom: 0,
80
+ right: 0,
81
+ toJSON: () => {
82
+ /* no-op */
83
+ },
84
+ })
85
+
86
+ useElementSize(() => el)
87
+ mountCallbacks.forEach((cb) => {
88
+ cb()
89
+ })
90
+
91
+ expect(observeSpy).toHaveBeenCalledWith(el)
92
+ })
93
+
94
+ it("updates size when ResizeObserver fires", () => {
95
+ const el = document.createElement("div")
96
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue({
97
+ width: 100,
98
+ height: 50,
99
+ x: 0,
100
+ y: 0,
101
+ top: 0,
102
+ left: 0,
103
+ bottom: 50,
104
+ right: 100,
105
+ toJSON: () => {
106
+ /* no-op */
107
+ },
108
+ })
109
+
110
+ const size = useElementSize(() => el)
111
+ mountCallbacks.forEach((cb) => {
112
+ cb()
113
+ })
114
+ expect(size().width).toBe(100)
115
+
116
+ // Simulate resize
117
+ resizeCallback?.([
118
+ {
119
+ contentRect: { width: 300, height: 150 },
120
+ } as unknown as ResizeObserverEntry,
121
+ ])
122
+
123
+ expect(size().width).toBe(300)
124
+ expect(size().height).toBe(150)
125
+ })
126
+
127
+ it("does nothing on mount when element is null", () => {
128
+ useElementSize(() => null)
129
+ mountCallbacks.forEach((cb) => {
130
+ cb()
131
+ })
132
+
133
+ expect(observeSpy).not.toHaveBeenCalled()
134
+ })
135
+
136
+ it("disconnects ResizeObserver on unmount", () => {
137
+ const el = document.createElement("div")
138
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue({
139
+ width: 0,
140
+ height: 0,
141
+ x: 0,
142
+ y: 0,
143
+ top: 0,
144
+ left: 0,
145
+ bottom: 0,
146
+ right: 0,
147
+ toJSON: () => {
148
+ /* no-op */
149
+ },
150
+ })
151
+
152
+ useElementSize(() => el)
153
+ mountCallbacks.forEach((cb) => {
154
+ cb()
155
+ })
156
+ unmountCallbacks.forEach((cb) => {
157
+ cb()
158
+ })
159
+
160
+ expect(disconnectSpy).toHaveBeenCalled()
161
+ })
162
+
163
+ it("handles ResizeObserver callback with no entry", () => {
164
+ const el = document.createElement("div")
165
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue({
166
+ width: 50,
167
+ height: 25,
168
+ x: 0,
169
+ y: 0,
170
+ top: 0,
171
+ left: 0,
172
+ bottom: 25,
173
+ right: 50,
174
+ toJSON: () => {
175
+ /* no-op */
176
+ },
177
+ })
178
+
179
+ const size = useElementSize(() => el)
180
+ mountCallbacks.forEach((cb) => {
181
+ cb()
182
+ })
183
+
184
+ // Empty entries array
185
+ resizeCallback?.([])
186
+ // Should keep initial measurement
187
+ expect(size().width).toBe(50)
188
+ expect(size().height).toBe(25)
189
+ })
190
+ })
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { useFocus } from "../useFocus"
3
+
4
+ describe("useFocus", () => {
5
+ it("initializes with focused=false", () => {
6
+ const { focused } = useFocus()
7
+ expect(focused()).toBe(false)
8
+ })
9
+
10
+ it("sets focused to true on focus", () => {
11
+ const { focused, props } = useFocus()
12
+ props.onFocus()
13
+ expect(focused()).toBe(true)
14
+ })
15
+
16
+ it("sets focused to false on blur", () => {
17
+ const { focused, props } = useFocus()
18
+ props.onFocus()
19
+ expect(focused()).toBe(true)
20
+ props.onBlur()
21
+ expect(focused()).toBe(false)
22
+ })
23
+
24
+ it("toggles focus state through multiple cycles", () => {
25
+ const { focused, props } = useFocus()
26
+ props.onFocus()
27
+ expect(focused()).toBe(true)
28
+ props.onBlur()
29
+ expect(focused()).toBe(false)
30
+ props.onFocus()
31
+ expect(focused()).toBe(true)
32
+ })
33
+
34
+ it("calling blur when not focused is safe", () => {
35
+ const { focused, props } = useFocus()
36
+ props.onBlur()
37
+ expect(focused()).toBe(false)
38
+ })
39
+
40
+ it("calling focus multiple times stays true", () => {
41
+ const { focused, props } = useFocus()
42
+ props.onFocus()
43
+ props.onFocus()
44
+ expect(focused()).toBe(true)
45
+ })
46
+
47
+ it("returns props object with onFocus and onBlur", () => {
48
+ const { props } = useFocus()
49
+ expect(typeof props.onFocus).toBe("function")
50
+ expect(typeof props.onBlur).toBe("function")
51
+ })
52
+ })
@@ -0,0 +1,162 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
+
3
+ let mountCallbacks: Array<() => void> = []
4
+ let unmountCallbacks: Array<() => void> = []
5
+
6
+ vi.mock("@pyreon/core", () => ({
7
+ onMount: (fn: () => unknown) => {
8
+ mountCallbacks.push(fn as () => void)
9
+ },
10
+ onUnmount: (fn: () => void) => {
11
+ unmountCallbacks.push(fn)
12
+ },
13
+ }))
14
+
15
+ import { useFocusTrap } from "../useFocusTrap"
16
+
17
+ describe("useFocusTrap", () => {
18
+ let container: HTMLDivElement
19
+ let btn1: HTMLButtonElement
20
+ let btn2: HTMLButtonElement
21
+ let btn3: HTMLButtonElement
22
+
23
+ beforeEach(() => {
24
+ mountCallbacks = []
25
+ unmountCallbacks = []
26
+ container = document.createElement("div")
27
+ btn1 = document.createElement("button")
28
+ btn2 = document.createElement("button")
29
+ btn3 = document.createElement("button")
30
+ container.append(btn1, btn2, btn3)
31
+ document.body.appendChild(container)
32
+ })
33
+
34
+ afterEach(() => {
35
+ document.body.removeChild(container)
36
+ })
37
+
38
+ it("wraps focus from last to first on Tab", () => {
39
+ useFocusTrap(() => container)
40
+ mountCallbacks.forEach((cb) => {
41
+ cb()
42
+ })
43
+
44
+ btn3.focus()
45
+ const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true })
46
+ const prevented = vi.spyOn(event, "preventDefault")
47
+ document.dispatchEvent(event)
48
+
49
+ expect(prevented).toHaveBeenCalled()
50
+ expect(document.activeElement).toBe(btn1)
51
+ })
52
+
53
+ it("wraps focus from first to last on Shift+Tab", () => {
54
+ useFocusTrap(() => container)
55
+ mountCallbacks.forEach((cb) => {
56
+ cb()
57
+ })
58
+
59
+ btn1.focus()
60
+ const event = new KeyboardEvent("keydown", {
61
+ key: "Tab",
62
+ shiftKey: true,
63
+ bubbles: true,
64
+ })
65
+ const prevented = vi.spyOn(event, "preventDefault")
66
+ document.dispatchEvent(event)
67
+
68
+ expect(prevented).toHaveBeenCalled()
69
+ expect(document.activeElement).toBe(btn3)
70
+ })
71
+
72
+ it("does not wrap when Tab on middle element", () => {
73
+ useFocusTrap(() => container)
74
+ mountCallbacks.forEach((cb) => {
75
+ cb()
76
+ })
77
+
78
+ btn2.focus()
79
+ const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true })
80
+ const prevented = vi.spyOn(event, "preventDefault")
81
+ document.dispatchEvent(event)
82
+
83
+ expect(prevented).not.toHaveBeenCalled()
84
+ })
85
+
86
+ it("ignores non-Tab keys", () => {
87
+ useFocusTrap(() => container)
88
+ mountCallbacks.forEach((cb) => {
89
+ cb()
90
+ })
91
+
92
+ btn3.focus()
93
+ const event = new KeyboardEvent("keydown", { key: "Enter", bubbles: true })
94
+ const prevented = vi.spyOn(event, "preventDefault")
95
+ document.dispatchEvent(event)
96
+
97
+ expect(prevented).not.toHaveBeenCalled()
98
+ })
99
+
100
+ it("does nothing when element is null", () => {
101
+ useFocusTrap(() => null)
102
+ mountCallbacks.forEach((cb) => {
103
+ cb()
104
+ })
105
+
106
+ const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true })
107
+ const prevented = vi.spyOn(event, "preventDefault")
108
+ document.dispatchEvent(event)
109
+
110
+ expect(prevented).not.toHaveBeenCalled()
111
+ })
112
+
113
+ it("does nothing when container has no focusable children", () => {
114
+ const emptyContainer = document.createElement("div")
115
+ emptyContainer.appendChild(document.createElement("div"))
116
+ document.body.appendChild(emptyContainer)
117
+
118
+ useFocusTrap(() => emptyContainer)
119
+ mountCallbacks.forEach((cb) => {
120
+ cb()
121
+ })
122
+
123
+ const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true })
124
+ const prevented = vi.spyOn(event, "preventDefault")
125
+ document.dispatchEvent(event)
126
+
127
+ expect(prevented).not.toHaveBeenCalled()
128
+ document.body.removeChild(emptyContainer)
129
+ })
130
+
131
+ it("cleans up event listener on unmount", () => {
132
+ const removeSpy = vi.spyOn(document, "removeEventListener")
133
+ useFocusTrap(() => container)
134
+ mountCallbacks.forEach((cb) => {
135
+ cb()
136
+ })
137
+ unmountCallbacks.forEach((cb) => {
138
+ cb()
139
+ })
140
+
141
+ expect(removeSpy).toHaveBeenCalledWith("keydown", expect.any(Function))
142
+ removeSpy.mockRestore()
143
+ })
144
+
145
+ it("does not prevent default when Shift+Tab on non-first element", () => {
146
+ useFocusTrap(() => container)
147
+ mountCallbacks.forEach((cb) => {
148
+ cb()
149
+ })
150
+
151
+ btn2.focus()
152
+ const event = new KeyboardEvent("keydown", {
153
+ key: "Tab",
154
+ shiftKey: true,
155
+ bubbles: true,
156
+ })
157
+ const prevented = vi.spyOn(event, "preventDefault")
158
+ document.dispatchEvent(event)
159
+
160
+ expect(prevented).not.toHaveBeenCalled()
161
+ })
162
+ })
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { useHover } from "../useHover"
3
+
4
+ describe("useHover", () => {
5
+ it("initializes with hovered=false", () => {
6
+ const { hovered } = useHover()
7
+ expect(hovered()).toBe(false)
8
+ })
9
+
10
+ it("sets hovered to true on mouseEnter", () => {
11
+ const { hovered, props } = useHover()
12
+ props.onMouseEnter()
13
+ expect(hovered()).toBe(true)
14
+ })
15
+
16
+ it("sets hovered to false on mouseLeave", () => {
17
+ const { hovered, props } = useHover()
18
+ props.onMouseEnter()
19
+ expect(hovered()).toBe(true)
20
+ props.onMouseLeave()
21
+ expect(hovered()).toBe(false)
22
+ })
23
+
24
+ it("toggles hover state correctly through multiple cycles", () => {
25
+ const { hovered, props } = useHover()
26
+ props.onMouseEnter()
27
+ expect(hovered()).toBe(true)
28
+ props.onMouseLeave()
29
+ expect(hovered()).toBe(false)
30
+ props.onMouseEnter()
31
+ expect(hovered()).toBe(true)
32
+ props.onMouseLeave()
33
+ expect(hovered()).toBe(false)
34
+ })
35
+
36
+ it("calling mouseLeave when already not hovered is safe", () => {
37
+ const { hovered, props } = useHover()
38
+ props.onMouseLeave()
39
+ expect(hovered()).toBe(false)
40
+ })
41
+
42
+ it("calling mouseEnter multiple times stays true", () => {
43
+ const { hovered, props } = useHover()
44
+ props.onMouseEnter()
45
+ props.onMouseEnter()
46
+ expect(hovered()).toBe(true)
47
+ })
48
+
49
+ it("returns props object with onMouseEnter and onMouseLeave", () => {
50
+ const { props } = useHover()
51
+ expect(typeof props.onMouseEnter).toBe("function")
52
+ expect(typeof props.onMouseLeave).toBe("function")
53
+ })
54
+
55
+ it("returns stable handler references", () => {
56
+ const { props } = useHover()
57
+ const enter = props.onMouseEnter
58
+ const leave = props.onMouseLeave
59
+ expect(props.onMouseEnter).toBe(enter)
60
+ expect(props.onMouseLeave).toBe(leave)
61
+ })
62
+ })
@@ -0,0 +1,154 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest"
2
+
3
+ let mountCallbacks: Array<() => unknown> = []
4
+ let unmountCallbacks: Array<() => void> = []
5
+
6
+ vi.mock("@pyreon/core", () => ({
7
+ onMount: (fn: () => unknown) => {
8
+ mountCallbacks.push(fn)
9
+ },
10
+ onUnmount: (fn: () => void) => {
11
+ unmountCallbacks.push(fn)
12
+ },
13
+ }))
14
+
15
+ import { useIntersection } from "../useIntersection"
16
+
17
+ describe("useIntersection", () => {
18
+ let intersectionCallback: ((entries: IntersectionObserverEntry[]) => void) | undefined
19
+ let observeSpy: ReturnType<typeof vi.fn>
20
+ let disconnectSpy: ReturnType<typeof vi.fn>
21
+
22
+ beforeEach(() => {
23
+ mountCallbacks = []
24
+ unmountCallbacks = []
25
+ intersectionCallback = undefined
26
+ observeSpy = vi.fn()
27
+ disconnectSpy = vi.fn()
28
+
29
+ globalThis.IntersectionObserver = vi.fn(function (
30
+ this: unknown,
31
+ cb: IntersectionObserverCallback,
32
+ options?: IntersectionObserverInit,
33
+ ) {
34
+ intersectionCallback = cb as (entries: IntersectionObserverEntry[]) => void
35
+ return {
36
+ observe: observeSpy,
37
+ unobserve: vi.fn(),
38
+ disconnect: disconnectSpy,
39
+ root: options?.root ?? null,
40
+ rootMargin: options?.rootMargin ?? "0px",
41
+ thresholds: Array.isArray(options?.threshold)
42
+ ? options.threshold
43
+ : [options?.threshold ?? 0],
44
+ takeRecords: vi.fn(() => []),
45
+ }
46
+ }) as unknown as typeof IntersectionObserver
47
+ })
48
+
49
+ it("returns null initially", () => {
50
+ const entry = useIntersection(() => null)
51
+ expect(entry()).toBeNull()
52
+ })
53
+
54
+ it("observes the element on mount", () => {
55
+ const el = document.createElement("div")
56
+ useIntersection(() => el)
57
+ mountCallbacks.forEach((cb) => {
58
+ cb()
59
+ })
60
+
61
+ expect(observeSpy).toHaveBeenCalledWith(el)
62
+ })
63
+
64
+ it("does not observe when element is null", () => {
65
+ useIntersection(() => null)
66
+ mountCallbacks.forEach((cb) => {
67
+ cb()
68
+ })
69
+
70
+ expect(observeSpy).not.toHaveBeenCalled()
71
+ })
72
+
73
+ it("updates entry when intersection changes", () => {
74
+ const el = document.createElement("div")
75
+ const entrySignal = useIntersection(() => el)
76
+ mountCallbacks.forEach((cb) => {
77
+ cb()
78
+ })
79
+
80
+ const mockEntry = {
81
+ isIntersecting: true,
82
+ intersectionRatio: 0.5,
83
+ target: el,
84
+ boundingClientRect: el.getBoundingClientRect(),
85
+ intersectionRect: el.getBoundingClientRect(),
86
+ rootBounds: null,
87
+ time: Date.now(),
88
+ } as unknown as IntersectionObserverEntry
89
+
90
+ intersectionCallback?.([mockEntry])
91
+ expect(entrySignal()).toBe(mockEntry)
92
+ expect(entrySignal()?.isIntersecting).toBe(true)
93
+ })
94
+
95
+ it("passes options to IntersectionObserver", () => {
96
+ const el = document.createElement("div")
97
+ const options = { threshold: 0.5, rootMargin: "10px" }
98
+ useIntersection(() => el, options)
99
+ mountCallbacks.forEach((cb) => {
100
+ cb()
101
+ })
102
+
103
+ expect(IntersectionObserver).toHaveBeenCalledWith(expect.any(Function), options)
104
+ })
105
+
106
+ it("disconnects observer on unmount", () => {
107
+ const el = document.createElement("div")
108
+ useIntersection(() => el)
109
+ mountCallbacks.forEach((cb) => {
110
+ cb()
111
+ })
112
+ unmountCallbacks.forEach((cb) => {
113
+ cb()
114
+ })
115
+
116
+ expect(disconnectSpy).toHaveBeenCalled()
117
+ })
118
+
119
+ it("does not crash when callback has empty entries", () => {
120
+ const el = document.createElement("div")
121
+ const entrySignal = useIntersection(() => el)
122
+ mountCallbacks.forEach((cb) => {
123
+ cb()
124
+ })
125
+
126
+ // Empty entries - e is undefined, so entry.set should not be called
127
+ intersectionCallback?.([])
128
+ expect(entrySignal()).toBeNull()
129
+ })
130
+
131
+ it("updates to latest entry on subsequent intersections", () => {
132
+ const el = document.createElement("div")
133
+ const entrySignal = useIntersection(() => el)
134
+ mountCallbacks.forEach((cb) => {
135
+ cb()
136
+ })
137
+
138
+ const entry1 = {
139
+ isIntersecting: true,
140
+ intersectionRatio: 0.3,
141
+ } as unknown as IntersectionObserverEntry
142
+
143
+ const entry2 = {
144
+ isIntersecting: false,
145
+ intersectionRatio: 0,
146
+ } as unknown as IntersectionObserverEntry
147
+
148
+ intersectionCallback?.([entry1])
149
+ expect(entrySignal()).toBe(entry1)
150
+
151
+ intersectionCallback?.([entry2])
152
+ expect(entrySignal()).toBe(entry2)
153
+ })
154
+ })
@@ -0,0 +1,48 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
+ import { useInterval } from "../useInterval"
3
+
4
+ // Mock onUnmount since it requires component lifecycle context
5
+ vi.mock("@pyreon/core", () => ({
6
+ onMount: (fn: () => void) => fn(),
7
+ onUnmount: (_fn: () => void) => {
8
+ /* no-op */
9
+ },
10
+ }))
11
+
12
+ describe("useInterval", () => {
13
+ beforeEach(() => vi.useFakeTimers())
14
+ afterEach(() => vi.useRealTimers())
15
+
16
+ it("calls callback at the specified interval", () => {
17
+ const fn = vi.fn()
18
+ useInterval(fn, 100)
19
+
20
+ vi.advanceTimersByTime(100)
21
+ expect(fn).toHaveBeenCalledTimes(1)
22
+
23
+ vi.advanceTimersByTime(100)
24
+ expect(fn).toHaveBeenCalledTimes(2)
25
+ })
26
+
27
+ it("does not call callback when delay is null", () => {
28
+ const fn = vi.fn()
29
+ useInterval(fn, null)
30
+
31
+ vi.advanceTimersByTime(1000)
32
+ expect(fn).not.toHaveBeenCalled()
33
+ })
34
+
35
+ it("calls the latest callback", () => {
36
+ let value = 0
37
+ let currentCb = () => {
38
+ value = 1
39
+ }
40
+ useInterval(() => currentCb(), 100)
41
+
42
+ currentCb = () => {
43
+ value = 2
44
+ }
45
+ vi.advanceTimersByTime(100)
46
+ expect(value).toBe(2)
47
+ })
48
+ })
@@ -0,0 +1,9 @@
1
+ import { onMount } from "@pyreon/core"
2
+ import { describe, expect, it } from "vitest"
3
+ import useIsomorphicLayoutEffect from "../useIsomorphicLayoutEffect"
4
+
5
+ describe("useIsomorphicLayoutEffect", () => {
6
+ it("is onMount in a browser environment", () => {
7
+ expect(useIsomorphicLayoutEffect).toBe(onMount)
8
+ })
9
+ })