@windstream/react-shared-components 0.1.93 → 0.1.95

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 (112) hide show
  1. package/dist/contentful/index.esm.js +2 -2
  2. package/dist/contentful/index.esm.js.map +1 -1
  3. package/dist/contentful/index.js +3 -3
  4. package/dist/contentful/index.js.map +1 -1
  5. package/dist/core.d.ts +2 -2
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.esm.js +1 -1
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/index.js +3 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/styles.css +1 -1
  12. package/dist/utils/index.esm.js +1 -1
  13. package/dist/utils/index.js +1 -1
  14. package/package.json +14 -8
  15. package/src/components/accordion/index.test.tsx +270 -0
  16. package/src/components/alert-card/index.test.tsx +152 -0
  17. package/src/components/animation-wrapper/index.test.tsx +424 -0
  18. package/src/components/brand-button/index.test.tsx +292 -0
  19. package/src/components/button/index.test.tsx +91 -0
  20. package/src/components/call-button/index.test.tsx +260 -0
  21. package/src/components/checkbox/index.test.tsx +252 -0
  22. package/src/components/checklist/index.test.tsx +231 -0
  23. package/src/components/checklist/index.tsx +64 -29
  24. package/src/components/checklist/types.ts +7 -1
  25. package/src/components/collapse/index.test.tsx +277 -0
  26. package/src/components/collapse/index.tsx +1 -0
  27. package/src/components/divider/index.test.tsx +53 -0
  28. package/src/components/image/index.test.tsx +174 -0
  29. package/src/components/input/index.test.tsx +348 -0
  30. package/src/components/link/index.test.tsx +199 -0
  31. package/src/components/list/index.test.tsx +166 -0
  32. package/src/components/material-icon/index.test.tsx +130 -0
  33. package/src/components/modal/index.test.tsx +310 -0
  34. package/src/components/next-image/index.test.tsx +406 -0
  35. package/src/components/pagination/index.test.tsx +521 -0
  36. package/src/components/radio-button/index.test.tsx +151 -0
  37. package/src/components/see-more/index.test.tsx +96 -0
  38. package/src/components/select/index.test.tsx +256 -0
  39. package/src/components/select-plan-button/index.test.tsx +173 -0
  40. package/src/components/skeleton/index.test.tsx +74 -0
  41. package/src/components/spinner/index.test.tsx +76 -0
  42. package/src/components/text/index.test.tsx +65 -0
  43. package/src/components/tooltip/index.test.tsx +50 -0
  44. package/src/components/view-cart-button/index.test.tsx +57 -0
  45. package/src/contentful/blocks/accordion/index.test.tsx +218 -0
  46. package/src/contentful/blocks/accordion/index.tsx +3 -1
  47. package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -0
  48. package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -0
  49. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +5 -4
  50. package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -0
  51. package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -0
  52. package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -0
  53. package/src/contentful/blocks/button/index.test.tsx +339 -0
  54. package/src/contentful/blocks/callout/index.test.tsx +539 -0
  55. package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -0
  56. package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -0
  57. package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -0
  58. package/src/contentful/blocks/cards/index.test.tsx +39 -0
  59. package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -0
  60. package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -0
  61. package/src/contentful/blocks/cards/simple-card/index.tsx +1 -1
  62. package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -0
  63. package/src/contentful/blocks/carousel/helper.test.tsx +539 -0
  64. package/src/contentful/blocks/carousel/index.test.tsx +308 -0
  65. package/src/contentful/blocks/carousel/types.test.ts +16 -0
  66. package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -0
  67. package/src/contentful/blocks/cart-retention-banner/index.tsx +4 -4
  68. package/src/contentful/blocks/comparison-table/index.test.tsx +114 -0
  69. package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -0
  70. package/src/contentful/blocks/cta-callout/index.test.tsx +244 -0
  71. package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -0
  72. package/src/contentful/blocks/email-input-block/index.test.tsx +213 -0
  73. package/src/contentful/blocks/email-input-block/index.tsx +40 -35
  74. package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -0
  75. package/src/contentful/blocks/floating-banner/index.test.tsx +246 -0
  76. package/src/contentful/blocks/footer/index.test.tsx +302 -0
  77. package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -0
  78. package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -0
  79. package/src/contentful/blocks/image-promo-bar/index.tsx +248 -246
  80. package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -0
  81. package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -0
  82. package/src/contentful/blocks/modal/index.test.tsx +209 -0
  83. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -0
  84. package/src/contentful/blocks/navigation/index.test.tsx +924 -0
  85. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -0
  86. package/src/contentful/blocks/primary-hero/index.test.tsx +286 -0
  87. package/src/contentful/blocks/primary-hero/index.tsx +7 -4
  88. package/src/contentful/blocks/search-block/index.test.tsx +268 -0
  89. package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -0
  90. package/src/contentful/blocks/text/index.test.tsx +36 -0
  91. package/src/contentful/index.test.ts +45 -0
  92. package/src/global-mocks/contentful/to-document.ts +25 -0
  93. package/src/global-mocks/cookie.ts +48 -0
  94. package/src/global-mocks/cx.ts +37 -0
  95. package/src/global-mocks/index.ts +89 -0
  96. package/src/global-mocks/speed-card-bg.ts +27 -0
  97. package/src/global-mocks/utm.ts +49 -0
  98. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -0
  99. package/src/hooks/contentful/use-contentful-rich-text.tsx +1 -1
  100. package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -0
  101. package/src/hooks/use-body-scroll-lock.test.ts +134 -0
  102. package/src/hooks/use-carousel-swipe.test.ts +393 -0
  103. package/src/hooks/use-outside-click.test.ts +142 -0
  104. package/src/index.ts +1 -1
  105. package/src/next/index.test.ts +7 -0
  106. package/src/setupTests.ts +17 -11
  107. package/src/utils/contentful/to-document.test.ts +85 -0
  108. package/src/utils/cookie.test.ts +180 -0
  109. package/src/utils/cx.test.ts +90 -0
  110. package/src/utils/index.test.ts +115 -0
  111. package/src/utils/speed-card-bg.test.ts +46 -0
  112. package/src/utils/utm.test.ts +359 -0
@@ -0,0 +1,393 @@
1
+ import React from "react";
2
+ import { CarouselSwipeConfig, useCarouselSwipe } from "./use-carousel-swipe";
3
+
4
+ import { act, renderHook } from "@testing-library/react";
5
+
6
+ // Helper to create a mock TouchEvent
7
+ function makeTouchEvent(clientX: number): React.TouchEvent {
8
+ return {
9
+ touches: [{ clientX }],
10
+ } as unknown as React.TouchEvent;
11
+ }
12
+
13
+ describe("useCarouselSwipe", () => {
14
+ const defaultConfig: CarouselSwipeConfig = {
15
+ itemCount: 5,
16
+ enableAutoScroll: false,
17
+ };
18
+
19
+ beforeEach(() => {
20
+ jest.useFakeTimers();
21
+ });
22
+
23
+ afterEach(() => {
24
+ jest.useRealTimers();
25
+ });
26
+
27
+ describe("Initial state", () => {
28
+ it("returns currentIndex 0", () => {
29
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
30
+ expect(result.current.currentIndex).toBe(0);
31
+ });
32
+
33
+ it("returns swipeOffset 0", () => {
34
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
35
+ expect(result.current.swipeOffset).toBe(0);
36
+ });
37
+
38
+ it("returns isSwiping false", () => {
39
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
40
+ expect(result.current.isSwiping).toBe(false);
41
+ });
42
+
43
+ it("returns containerRef", () => {
44
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
45
+ expect(result.current.containerRef).toBeDefined();
46
+ expect(result.current.containerRef.current).toBeNull();
47
+ });
48
+
49
+ it("returns default constants", () => {
50
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
51
+ expect(result.current.constants).toEqual({
52
+ CARD_OFFSET_PERCENTAGE: 105,
53
+ SWIPE_THRESHOLD: 0.15,
54
+ MOBILE_BREAKPOINT: 768,
55
+ AUTO_SCROLL_INTERVAL: 8000,
56
+ });
57
+ });
58
+
59
+ it("uses custom constants from config", () => {
60
+ const { result } = renderHook(() =>
61
+ useCarouselSwipe({
62
+ itemCount: 3,
63
+ cardOffsetPercentage: 110,
64
+ swipeThreshold: 0.2,
65
+ mobileBreakpoint: 1024,
66
+ autoScrollInterval: 5000,
67
+ })
68
+ );
69
+ expect(result.current.constants).toEqual({
70
+ CARD_OFFSET_PERCENTAGE: 110,
71
+ SWIPE_THRESHOLD: 0.2,
72
+ MOBILE_BREAKPOINT: 1024,
73
+ AUTO_SCROLL_INTERVAL: 5000,
74
+ });
75
+ });
76
+ });
77
+
78
+ describe("nextSlide", () => {
79
+ it("advances to next slide", () => {
80
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
81
+ act(() => result.current.nextSlide());
82
+ expect(result.current.currentIndex).toBe(1);
83
+ });
84
+
85
+ it("wraps around to 0 from last slide", () => {
86
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
87
+ // Go to last slide (index 4)
88
+ act(() => result.current.goToSlide(4));
89
+ act(() => result.current.nextSlide());
90
+ expect(result.current.currentIndex).toBe(0);
91
+ });
92
+
93
+ it("does nothing when itemCount is 0", () => {
94
+ const { result } = renderHook(() =>
95
+ useCarouselSwipe({ itemCount: 0, enableAutoScroll: false })
96
+ );
97
+ act(() => result.current.nextSlide());
98
+ expect(result.current.currentIndex).toBe(0);
99
+ });
100
+ });
101
+
102
+ describe("prevSlide", () => {
103
+ it("goes to previous slide", () => {
104
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
105
+ act(() => result.current.goToSlide(2));
106
+ act(() => result.current.prevSlide());
107
+ expect(result.current.currentIndex).toBe(1);
108
+ });
109
+
110
+ it("wraps to last slide from first slide", () => {
111
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
112
+ act(() => result.current.prevSlide());
113
+ expect(result.current.currentIndex).toBe(4);
114
+ });
115
+
116
+ it("does nothing when itemCount is 0", () => {
117
+ const { result } = renderHook(() =>
118
+ useCarouselSwipe({ itemCount: 0, enableAutoScroll: false })
119
+ );
120
+ act(() => result.current.prevSlide());
121
+ expect(result.current.currentIndex).toBe(0);
122
+ });
123
+ });
124
+
125
+ describe("goToSlide", () => {
126
+ it("navigates to specific slide index", () => {
127
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
128
+ act(() => result.current.goToSlide(3));
129
+ expect(result.current.currentIndex).toBe(3);
130
+ });
131
+
132
+ it("ignores negative index", () => {
133
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
134
+ act(() => result.current.goToSlide(-1));
135
+ expect(result.current.currentIndex).toBe(0);
136
+ });
137
+
138
+ it("ignores index >= itemCount", () => {
139
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
140
+ act(() => result.current.goToSlide(5));
141
+ expect(result.current.currentIndex).toBe(0);
142
+ });
143
+
144
+ it("navigates to first slide (index 0)", () => {
145
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
146
+ act(() => result.current.goToSlide(3));
147
+ act(() => result.current.goToSlide(0));
148
+ expect(result.current.currentIndex).toBe(0);
149
+ });
150
+
151
+ it("navigates to last valid slide", () => {
152
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
153
+ act(() => result.current.goToSlide(4));
154
+ expect(result.current.currentIndex).toBe(4);
155
+ });
156
+ });
157
+
158
+ describe("Touch gesture handling", () => {
159
+ it("sets isSwiping to true on touch start", () => {
160
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
161
+ act(() => result.current.handleTouchStart(makeTouchEvent(200)));
162
+ expect(result.current.isSwiping).toBe(true);
163
+ });
164
+
165
+ it("tracks swipe offset during touch move", () => {
166
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
167
+ act(() => result.current.handleTouchStart(makeTouchEvent(200)));
168
+ act(() => result.current.handleTouchMove(makeTouchEvent(150)));
169
+ expect(result.current.swipeOffset).toBe(-50);
170
+ });
171
+
172
+ it("does not track move when not swiping", () => {
173
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
174
+ // Don't call handleTouchStart, so isSwiping is false
175
+ act(() => result.current.handleTouchMove(makeTouchEvent(150)));
176
+ expect(result.current.swipeOffset).toBe(0);
177
+ });
178
+
179
+ it("resets swipeOffset and isSwiping on touch end", () => {
180
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
181
+ act(() => result.current.handleTouchStart(makeTouchEvent(200)));
182
+ act(() => result.current.handleTouchMove(makeTouchEvent(210)));
183
+ act(() => result.current.handleTouchEnd());
184
+ expect(result.current.swipeOffset).toBe(0);
185
+ expect(result.current.isSwiping).toBe(false);
186
+ });
187
+
188
+ it("triggers prevSlide on right swipe exceeding threshold", () => {
189
+ const { result } = renderHook(() =>
190
+ useCarouselSwipe({ ...defaultConfig, swipeThreshold: 0.01 })
191
+ );
192
+ // Start at slide 2 so prevSlide can go back
193
+ act(() => result.current.goToSlide(2));
194
+ act(() => result.current.handleTouchStart(makeTouchEvent(100)));
195
+ act(() => result.current.handleTouchMove(makeTouchEvent(300)));
196
+ act(() => result.current.handleTouchEnd());
197
+ expect(result.current.currentIndex).toBe(1);
198
+ });
199
+
200
+ it("triggers nextSlide on left swipe exceeding threshold", () => {
201
+ const { result } = renderHook(() =>
202
+ useCarouselSwipe({ ...defaultConfig, swipeThreshold: 0.01 })
203
+ );
204
+ act(() => result.current.handleTouchStart(makeTouchEvent(300)));
205
+ act(() => result.current.handleTouchMove(makeTouchEvent(100)));
206
+ act(() => result.current.handleTouchEnd());
207
+ expect(result.current.currentIndex).toBe(1);
208
+ });
209
+
210
+ it("does not change slide when swipe is below threshold", () => {
211
+ const { result } = renderHook(() =>
212
+ useCarouselSwipe({ ...defaultConfig, swipeThreshold: 0.99 })
213
+ );
214
+ act(() => result.current.handleTouchStart(makeTouchEvent(200)));
215
+ act(() => result.current.handleTouchMove(makeTouchEvent(195)));
216
+ act(() => result.current.handleTouchEnd());
217
+ expect(result.current.currentIndex).toBe(0);
218
+ });
219
+ });
220
+
221
+ describe("Auto-scroll", () => {
222
+ it("auto-scrolls to next slide at configured interval", () => {
223
+ const { result } = renderHook(() =>
224
+ useCarouselSwipe({
225
+ itemCount: 5,
226
+ enableAutoScroll: true,
227
+ autoScrollInterval: 3000,
228
+ })
229
+ );
230
+ expect(result.current.currentIndex).toBe(0);
231
+ act(() => jest.advanceTimersByTime(3000));
232
+ expect(result.current.currentIndex).toBe(1);
233
+ act(() => jest.advanceTimersByTime(3000));
234
+ expect(result.current.currentIndex).toBe(2);
235
+ });
236
+
237
+ it("does not auto-scroll when enableAutoScroll is false", () => {
238
+ const { result } = renderHook(() =>
239
+ useCarouselSwipe({
240
+ itemCount: 5,
241
+ enableAutoScroll: false,
242
+ autoScrollInterval: 3000,
243
+ })
244
+ );
245
+ act(() => jest.advanceTimersByTime(10000));
246
+ expect(result.current.currentIndex).toBe(0);
247
+ });
248
+
249
+ it("does not auto-scroll when autoScrollInterval is 0", () => {
250
+ const { result } = renderHook(() =>
251
+ useCarouselSwipe({
252
+ itemCount: 5,
253
+ enableAutoScroll: true,
254
+ autoScrollInterval: 0,
255
+ })
256
+ );
257
+ act(() => jest.advanceTimersByTime(10000));
258
+ expect(result.current.currentIndex).toBe(0);
259
+ });
260
+
261
+ it("does not auto-scroll when itemCount is 0", () => {
262
+ const { result } = renderHook(() =>
263
+ useCarouselSwipe({
264
+ itemCount: 0,
265
+ enableAutoScroll: true,
266
+ autoScrollInterval: 3000,
267
+ })
268
+ );
269
+ act(() => jest.advanceTimersByTime(10000));
270
+ expect(result.current.currentIndex).toBe(0);
271
+ });
272
+
273
+ it("pauses auto-scroll on touch start", () => {
274
+ const { result } = renderHook(() =>
275
+ useCarouselSwipe({
276
+ itemCount: 5,
277
+ enableAutoScroll: true,
278
+ autoScrollInterval: 3000,
279
+ })
280
+ );
281
+ act(() => result.current.handleTouchStart(makeTouchEvent(200)));
282
+ act(() => jest.advanceTimersByTime(10000));
283
+ // Should still be at 0 because auto-scroll was paused
284
+ // (though the initial interval may have already ticked)
285
+ expect(result.current.isSwiping).toBe(true);
286
+ });
287
+
288
+ it("restarts auto-scroll after touch end when enabled", () => {
289
+ const { result } = renderHook(() =>
290
+ useCarouselSwipe({
291
+ itemCount: 5,
292
+ enableAutoScroll: true,
293
+ autoScrollInterval: 3000,
294
+ })
295
+ );
296
+ // Touch interaction
297
+ act(() => result.current.handleTouchStart(makeTouchEvent(200)));
298
+ act(() => result.current.handleTouchEnd());
299
+ // After touch end, auto-scroll should restart
300
+ act(() => jest.advanceTimersByTime(3000));
301
+ expect(result.current.currentIndex).toBeGreaterThan(0);
302
+ });
303
+
304
+ it("cleans up interval on unmount", () => {
305
+ const clearIntervalSpy = jest.spyOn(global, "clearInterval");
306
+ const { unmount } = renderHook(() =>
307
+ useCarouselSwipe({
308
+ itemCount: 5,
309
+ enableAutoScroll: true,
310
+ autoScrollInterval: 3000,
311
+ })
312
+ );
313
+ unmount();
314
+ expect(clearIntervalSpy).toHaveBeenCalled();
315
+ clearIntervalSpy.mockRestore();
316
+ });
317
+ });
318
+
319
+ describe("Responsive behavior", () => {
320
+ it("detects mobile viewport", () => {
321
+ Object.defineProperty(window, "innerWidth", {
322
+ writable: true,
323
+ value: 500,
324
+ });
325
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
326
+ expect(result.current.isMobile).toBe(true);
327
+ });
328
+
329
+ it("detects desktop viewport", () => {
330
+ Object.defineProperty(window, "innerWidth", {
331
+ writable: true,
332
+ value: 1024,
333
+ });
334
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
335
+ expect(result.current.isMobile).toBe(false);
336
+ });
337
+
338
+ it("updates isMobile on resize", () => {
339
+ Object.defineProperty(window, "innerWidth", {
340
+ writable: true,
341
+ value: 1024,
342
+ });
343
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
344
+ expect(result.current.isMobile).toBe(false);
345
+
346
+ act(() => {
347
+ Object.defineProperty(window, "innerWidth", {
348
+ writable: true,
349
+ value: 500,
350
+ });
351
+ window.dispatchEvent(new Event("resize"));
352
+ });
353
+ expect(result.current.isMobile).toBe(true);
354
+ });
355
+
356
+ it("updates containerWidth on resize", () => {
357
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
358
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
359
+ const initialWidth = result.current.containerWidth;
360
+ act(() => {
361
+ Object.defineProperty(window, "innerWidth", {
362
+ writable: true,
363
+ value: 800,
364
+ });
365
+ window.dispatchEvent(new Event("resize"));
366
+ });
367
+ // containerRef is null so falls back to window.innerWidth
368
+ expect(result.current.containerWidth).toBe(800);
369
+ });
370
+
371
+ it("uses custom mobileBreakpoint", () => {
372
+ Object.defineProperty(window, "innerWidth", {
373
+ writable: true,
374
+ value: 900,
375
+ });
376
+ const { result } = renderHook(() =>
377
+ useCarouselSwipe({ ...defaultConfig, mobileBreakpoint: 1024 })
378
+ );
379
+ expect(result.current.isMobile).toBe(true);
380
+ });
381
+
382
+ it("removes resize listeners on unmount", () => {
383
+ const removeEventListenerSpy = jest.spyOn(window, "removeEventListener");
384
+ const { unmount } = renderHook(() => useCarouselSwipe(defaultConfig));
385
+ unmount();
386
+ const resizeCalls = removeEventListenerSpy.mock.calls.filter(
387
+ call => call[0] === "resize"
388
+ );
389
+ expect(resizeCalls.length).toBeGreaterThanOrEqual(2); // containerWidth + isMobile
390
+ removeEventListenerSpy.mockRestore();
391
+ });
392
+ });
393
+ });
@@ -0,0 +1,142 @@
1
+ import React from "react";
2
+ import { useOutsideClick } from "./use-outside-click";
3
+
4
+ import { renderHook } from "@testing-library/react";
5
+
6
+ describe("useOutsideClick", () => {
7
+ let callback: jest.Mock;
8
+
9
+ beforeEach(() => {
10
+ callback = jest.fn();
11
+ });
12
+
13
+ describe("Event listener lifecycle", () => {
14
+ it("adds click event listener on mount", () => {
15
+ const addSpy = jest.spyOn(document, "addEventListener");
16
+ const ref = { current: document.createElement("div") };
17
+ renderHook(() => useOutsideClick(ref, callback));
18
+ const clickCalls = addSpy.mock.calls.filter(c => c[0] === "click");
19
+ expect(clickCalls.length).toBeGreaterThanOrEqual(1);
20
+ addSpy.mockRestore();
21
+ });
22
+
23
+ it("removes click event listener on unmount", () => {
24
+ const removeSpy = jest.spyOn(document, "removeEventListener");
25
+ const ref = { current: document.createElement("div") };
26
+ const { unmount } = renderHook(() => useOutsideClick(ref, callback));
27
+ unmount();
28
+ const clickCalls = removeSpy.mock.calls.filter(c => c[0] === "click");
29
+ expect(clickCalls.length).toBeGreaterThanOrEqual(1);
30
+ removeSpy.mockRestore();
31
+ });
32
+ });
33
+
34
+ describe("Click detection", () => {
35
+ it("calls callback when clicking outside the ref element", () => {
36
+ const inside = document.createElement("div");
37
+ const outside = document.createElement("button");
38
+ document.body.appendChild(inside);
39
+ document.body.appendChild(outside);
40
+
41
+ const ref = { current: inside };
42
+ renderHook(() => useOutsideClick(ref, callback));
43
+
44
+ outside.click();
45
+ expect(callback).toHaveBeenCalledTimes(1);
46
+
47
+ document.body.removeChild(inside);
48
+ document.body.removeChild(outside);
49
+ });
50
+
51
+ it("does not call callback when clicking inside the ref element", () => {
52
+ const container = document.createElement("div");
53
+ const child = document.createElement("span");
54
+ container.appendChild(child);
55
+ document.body.appendChild(container);
56
+
57
+ const ref = { current: container };
58
+ renderHook(() => useOutsideClick(ref, callback));
59
+
60
+ child.click();
61
+ expect(callback).not.toHaveBeenCalled();
62
+
63
+ document.body.removeChild(container);
64
+ });
65
+
66
+ it("does not call callback when clicking on the ref element itself", () => {
67
+ const el = document.createElement("div");
68
+ document.body.appendChild(el);
69
+
70
+ const ref = { current: el };
71
+ renderHook(() => useOutsideClick(ref, callback));
72
+
73
+ el.click();
74
+ expect(callback).not.toHaveBeenCalled();
75
+
76
+ document.body.removeChild(el);
77
+ });
78
+
79
+ it("calls callback when ref.current is null", () => {
80
+ const ref = { current: null } as React.RefObject<any>;
81
+ renderHook(() => useOutsideClick(ref, callback));
82
+
83
+ document.body.click();
84
+ expect(callback).toHaveBeenCalledTimes(1);
85
+ });
86
+
87
+ it("calls callback when ref is undefined-like", () => {
88
+ const ref = {} as React.RefObject<any>;
89
+ renderHook(() => useOutsideClick(ref, callback));
90
+
91
+ document.body.click();
92
+ expect(callback).toHaveBeenCalledTimes(1);
93
+ });
94
+
95
+ it("calls callback when ref is null", () => {
96
+ renderHook(() => useOutsideClick(null as any, callback));
97
+ document.body.click();
98
+ expect(callback).toHaveBeenCalledTimes(1);
99
+ });
100
+ });
101
+
102
+ describe("Multiple clicks", () => {
103
+ it("calls callback for each outside click", () => {
104
+ const inside = document.createElement("div");
105
+ document.body.appendChild(inside);
106
+
107
+ const ref = { current: inside };
108
+ renderHook(() => useOutsideClick(ref, callback));
109
+
110
+ document.body.click();
111
+ document.body.click();
112
+ document.body.click();
113
+ expect(callback).toHaveBeenCalledTimes(3);
114
+
115
+ document.body.removeChild(inside);
116
+ });
117
+ });
118
+
119
+ describe("Re-render behavior", () => {
120
+ it("updates listener on re-render with new callback", () => {
121
+ const el = document.createElement("div");
122
+ document.body.appendChild(el);
123
+
124
+ const callback1 = jest.fn();
125
+ const callback2 = jest.fn();
126
+ const ref = { current: el };
127
+
128
+ const { rerender } = renderHook(({ cb }) => useOutsideClick(ref, cb), {
129
+ initialProps: { cb: callback1 },
130
+ });
131
+
132
+ document.body.click();
133
+ expect(callback1).toHaveBeenCalledTimes(1);
134
+
135
+ rerender({ cb: callback2 });
136
+ document.body.click();
137
+ expect(callback2).toHaveBeenCalledTimes(1);
138
+
139
+ document.body.removeChild(el);
140
+ });
141
+ });
142
+ });
package/src/index.ts CHANGED
@@ -60,7 +60,7 @@ export type {
60
60
  } from "./components/brand-button";
61
61
 
62
62
  export { Checklist } from "./components/checklist";
63
- export type { ChecklistProps } from "./components/checklist";
63
+ export type { ChecklistItem, ChecklistProps } from "./components/checklist";
64
64
 
65
65
  export { Collapse } from "./components/collapse";
66
66
  export type { CollapsibleProps } from "./components/collapse";
@@ -0,0 +1,7 @@
1
+ import * as NextExports from "./index";
2
+
3
+ describe("next barrel exports", () => {
4
+ it("exports NextImage component", () => {
5
+ expect(NextExports.NextImage).toBeDefined();
6
+ });
7
+ });
package/src/setupTests.ts CHANGED
@@ -3,43 +3,49 @@ import "@testing-library/jest-dom";
3
3
  // Mock window.matchMedia
4
4
  Object.defineProperty(window, "matchMedia", {
5
5
  writable: true,
6
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
6
7
  value: (query: string) => ({
7
8
  matches: false,
8
9
  media: query,
9
10
  onchange: null,
10
- addListener: () => {}, // deprecated
11
- removeListener: () => {}, // deprecated
12
- addEventListener: () => {},
13
- removeEventListener: () => {},
11
+ addListener: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
12
+ removeListener: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
13
+ addEventListener: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
14
+ removeEventListener: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
14
15
  dispatchEvent: () => true,
15
16
  }),
16
17
  });
17
18
 
18
19
  // Mock ResizeObserver
20
+ // eslint-disable-next-line no-undef, @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
19
21
  global.ResizeObserver = class ResizeObserver {
20
- observe = () => {};
21
- unobserve = () => {};
22
- disconnect = () => {};
22
+ observe = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
23
+ unobserve = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
24
+ disconnect = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
25
+ // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars, no-undef
23
26
  constructor(_callback: ResizeObserverCallback) {}
24
27
  } as unknown as typeof ResizeObserver;
25
28
 
26
29
  // Mock IntersectionObserver
30
+ // eslint-disable-next-line no-undef, @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
27
31
  global.IntersectionObserver = class IntersectionObserver {
28
- observe = () => {};
29
- unobserve = () => {};
30
- disconnect = () => {};
32
+ observe = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
33
+ unobserve = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
34
+ disconnect = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
35
+ // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars, no-undef
31
36
  constructor(_callback: IntersectionObserverCallback) {}
32
37
  } as unknown as typeof IntersectionObserver;
33
38
 
34
39
  // Mock scrollTo
35
40
  Object.defineProperty(window, "scrollTo", {
36
41
  writable: true,
37
- value: () => {},
42
+ value: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
38
43
  });
39
44
 
40
45
  // Mock getComputedStyle
41
46
  Object.defineProperty(window, "getComputedStyle", {
42
47
  writable: true,
48
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
43
49
  value: () => ({
44
50
  getPropertyValue: () => "",
45
51
  }),
@@ -0,0 +1,85 @@
1
+ import { toDocument } from "./to-document";
2
+
3
+ import { BLOCKS, type Document } from "@contentful/rich-text-types";
4
+
5
+ const makeDocument = (text = "Hello"): Document => ({
6
+ nodeType: BLOCKS.DOCUMENT,
7
+ data: {},
8
+ content: [
9
+ {
10
+ nodeType: BLOCKS.PARAGRAPH,
11
+ data: {},
12
+ content: [{ nodeType: "text", value: text, marks: [], data: {} }],
13
+ },
14
+ ],
15
+ });
16
+
17
+ describe("toDocument", () => {
18
+ // ── Falsy inputs ─────────────────────────────────────
19
+ it("returns null for null", () => {
20
+ expect(toDocument(null)).toBeNull();
21
+ });
22
+
23
+ it("returns null for undefined", () => {
24
+ expect(toDocument(undefined)).toBeNull();
25
+ });
26
+
27
+ it("returns null for empty string", () => {
28
+ expect(toDocument("")).toBeNull();
29
+ });
30
+
31
+ it("returns null for 0", () => {
32
+ expect(toDocument(0)).toBeNull();
33
+ });
34
+
35
+ it("returns null for false", () => {
36
+ expect(toDocument(false)).toBeNull();
37
+ });
38
+
39
+ // ── REST/CDA Document ────────────────────────────────
40
+ it("returns Document directly when given a valid REST Document", () => {
41
+ const doc = makeDocument();
42
+ expect(toDocument(doc)).toBe(doc);
43
+ });
44
+
45
+ // ── GraphQL RichText { json: Document } ──────────────
46
+ it("extracts Document from GraphQL format { json, links }", () => {
47
+ const doc = makeDocument("GraphQL");
48
+ const graphql = { json: doc, links: { assets: [] } };
49
+ expect(toDocument(graphql)).toBe(doc);
50
+ });
51
+
52
+ it("extracts Document from GraphQL format without links", () => {
53
+ const doc = makeDocument("NoLinks");
54
+ expect(toDocument({ json: doc })).toBe(doc);
55
+ });
56
+
57
+ it("returns null when json property is not a valid Document", () => {
58
+ expect(toDocument({ json: "not-a-document" })).toBeNull();
59
+ });
60
+
61
+ it("returns null when json property is null", () => {
62
+ expect(toDocument({ json: null })).toBeNull();
63
+ });
64
+
65
+ // ── Non-matching objects ─────────────────────────────
66
+ it("returns null for an object without nodeType or json", () => {
67
+ expect(toDocument({ foo: "bar" })).toBeNull();
68
+ });
69
+
70
+ it("returns null for an object with nodeType but no content", () => {
71
+ expect(toDocument({ nodeType: BLOCKS.DOCUMENT })).toBeNull();
72
+ });
73
+
74
+ it("returns null for an array", () => {
75
+ expect(toDocument([1, 2, 3])).toBeNull();
76
+ });
77
+
78
+ it("returns null for a number", () => {
79
+ expect(toDocument(42)).toBeNull();
80
+ });
81
+
82
+ it("returns null for a non-empty string", () => {
83
+ expect(toDocument("hello")).toBeNull();
84
+ });
85
+ });