@windstream/react-shared-components 0.2.74 → 0.2.75

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.
@@ -1,3 +1,4 @@
1
+ import { useState } from "react";
1
2
  import { Carousel } from "./index";
2
3
  import type { carouselFieldsInterface } from "./types";
3
4
 
@@ -31,7 +32,8 @@ const sampleProduct = (
31
32
  productCardDescription = "Reliable internet for your home.",
32
33
  giftRewards: { list: { items: Array<{ title: string; icon: string }> } } = {
33
34
  list: { items: [] },
34
- }
35
+ },
36
+ productCategory?: string
35
37
  ) => ({
36
38
  __typename: "ComponentProductCard" as const,
37
39
  sys: { id },
@@ -41,6 +43,7 @@ const sampleProduct = (
41
43
  techType: "Fiber",
42
44
  highlighted,
43
45
  productCardDescription,
46
+ productCategory,
44
47
  benefitsTitle: "Plan benefits",
45
48
  benefitsExpanded: false,
46
49
  cta: { buttonLabel: "Select plan" },
@@ -83,8 +86,20 @@ const twoColumnFields: carouselFieldsInterface = {
83
86
  },
84
87
  }
85
88
  ),
86
- sampleProduct("plan-3", "1 Gig", "69", false, "Good for most day-to-day internet uses including streaming video."),
87
- sampleProduct("plan-4", "2 Gig", "89", false, "Worry-free internet for the whole home."),
89
+ sampleProduct(
90
+ "plan-3",
91
+ "1 Gig",
92
+ "69",
93
+ false,
94
+ "Good for most day-to-day internet uses including streaming video."
95
+ ),
96
+ sampleProduct(
97
+ "plan-4",
98
+ "2 Gig",
99
+ "89",
100
+ false,
101
+ "Worry-free internet for the whole home."
102
+ ),
88
103
  ],
89
104
  },
90
105
  };
@@ -106,3 +121,288 @@ export const TwoColumnProductCards: Story = {
106
121
  },
107
122
  };
108
123
 
124
+ const mastercardGift = (amount: string) => ({
125
+ list: {
126
+ items: [
127
+ {
128
+ title: `|$${amount}| prepaid Mastercard®`,
129
+ icon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Crect width='24' height='24' rx='4' fill='%23ffd200'/%3E%3C/svg%3E",
130
+ },
131
+ ],
132
+ },
133
+ });
134
+
135
+ const tabbedFields: carouselFieldsInterface = {
136
+ __typename: "ComponentCarousel",
137
+ sys: { id: "carousel-tabbed-gift" },
138
+ title: "fiber internet plans that fit your way of working",
139
+ twoColumn: false,
140
+ backgroundColor: "white",
141
+ items: {
142
+ items: [
143
+ // "Business Ready" tab — cards carry a gift badge, so the gift slot is
144
+ // reserved and the CTAs stay aligned.
145
+ sampleProduct(
146
+ "br-1",
147
+ "fiber 1 gig",
148
+ "99",
149
+ true,
150
+ "Fast, secure connectivity solution for growing teams.",
151
+ mastercardGift("100"),
152
+ "Business Ready"
153
+ ),
154
+ sampleProduct(
155
+ "br-2",
156
+ "fiber 2 gig",
157
+ "129",
158
+ false,
159
+ "Ultra-fast connectivity solution for high-traffic businesses.",
160
+ mastercardGift("200"),
161
+ "Business Ready"
162
+ ),
163
+ // "Internet Only" tab — no gift badges, so the gift slot must collapse and
164
+ // the CTA should sit flush with the description (the fix under test).
165
+ sampleProduct(
166
+ "io-1",
167
+ "fiber 300 mbps",
168
+ "49",
169
+ true,
170
+ "Ideal for guest Wi-Fi, multiple POS devices, video uploads and streaming.",
171
+ { list: { items: [] } },
172
+ "Internet Only"
173
+ ),
174
+ ],
175
+ },
176
+ };
177
+
178
+ const TabbedTemplate = () => {
179
+ const [activeTab, setActiveTab] = useState("Business Ready");
180
+ return (
181
+ <div style={{ width: "100%", maxWidth: 960 }}>
182
+ <Carousel
183
+ fields={tabbedFields}
184
+ hasProductCards={true}
185
+ hasTestimonialCards={false}
186
+ disclaimerText={
187
+ "Price includes a $5.00/mo credit with AutoPay, option to disable at the next step."
188
+ }
189
+ backgroundColor="bg-white"
190
+ showSwitch={true}
191
+ activeTab={activeTab}
192
+ setActiveTab={setActiveTab}
193
+ tabs={["Business Ready", "Internet Only"]}
194
+ />
195
+ </div>
196
+ );
197
+ };
198
+
199
+ /**
200
+ * Reproduces the reported gap: switching to the "Internet Only" tab (whose cards
201
+ * have no gift badge) must collapse the gift slot so the Select plan CTA sits
202
+ * flush with the description, while the "Business Ready" tab (gift badges
203
+ * present) keeps the slot reserved so CTAs stay aligned.
204
+ */
205
+ export const TabbedGiftAlignment: Story = {
206
+ parameters: { layout: "fullscreen" },
207
+ render: () => <TabbedTemplate />,
208
+ };
209
+
210
+ const mixedGiftFields: carouselFieldsInterface = {
211
+ __typename: "ComponentCarousel",
212
+ sys: { id: "carousel-mixed-gift" },
213
+ title: "Mixed gift / no-gift cards",
214
+ subTitle: "The no-gift card reserves the gift height so both CTAs align.",
215
+ twoColumn: false,
216
+ backgroundColor: "white",
217
+ items: {
218
+ items: [
219
+ sampleProduct(
220
+ "mixed-1",
221
+ "fiber 1 gig",
222
+ "99",
223
+ true,
224
+ "Fast, secure connectivity solution for growing teams.",
225
+ mastercardGift("100")
226
+ ),
227
+ sampleProduct(
228
+ "mixed-2",
229
+ "fiber 2 gig",
230
+ "129",
231
+ false,
232
+ "Ultra-fast connectivity solution for high-traffic businesses."
233
+ ),
234
+ ],
235
+ },
236
+ };
237
+
238
+ /**
239
+ * One card has a single gift badge, the other has none. The no-gift card must
240
+ * reserve the gift-slot height so both Select plan CTAs sit at the same height,
241
+ * while the gift card's CTA sits directly below its badge (no over-reservation).
242
+ */
243
+ export const MixedGiftAlignment: Story = {
244
+ parameters: { layout: "fullscreen" },
245
+ args: {
246
+ fields: mixedGiftFields,
247
+ hasProductCards: true,
248
+ hasTestimonialCards: false,
249
+ disclaimerText: null,
250
+ backgroundColor: "bg-white",
251
+ activeTab: "",
252
+ setActiveTab: () => {},
253
+ tabs: [],
254
+ },
255
+ };
256
+
257
+ const tabbedMixedFields: carouselFieldsInterface = {
258
+ __typename: "ComponentCarousel",
259
+ sys: { id: "carousel-tabbed-mixed-gift" },
260
+ title: "fiber internet plans that fit your way of working",
261
+ twoColumn: false,
262
+ backgroundColor: "white",
263
+ items: {
264
+ items: [
265
+ // "Business Ready" tab — one card has a gift badge, one does not. The
266
+ // no-gift card must reserve the gift height so both CTAs align.
267
+ sampleProduct(
268
+ "tm-br-1",
269
+ "fiber 1 gig",
270
+ "99",
271
+ true,
272
+ "Fast, secure connectivity solution for growing teams.",
273
+ mastercardGift("100"),
274
+ "Business Ready"
275
+ ),
276
+ sampleProduct(
277
+ "tm-br-2",
278
+ "fiber 2 gig",
279
+ "129",
280
+ false,
281
+ "Ultra-fast connectivity solution for high-traffic businesses.",
282
+ { list: { items: [] } },
283
+ "Business Ready"
284
+ ),
285
+ // "Internet Only" tab — neither card has a gift badge, so both gift slots
286
+ // collapse and both CTAs sit flush with the description (recalculated for
287
+ // this tab on switch).
288
+ sampleProduct(
289
+ "tm-io-1",
290
+ "fiber 300 mbps",
291
+ "49",
292
+ false,
293
+ "Ideal for guest Wi-Fi, multiple POS devices, video uploads and streaming.",
294
+ { list: { items: [] } },
295
+ "Internet Only"
296
+ ),
297
+ sampleProduct(
298
+ "tm-io-2",
299
+ "fiber 500 mbps",
300
+ "59",
301
+ true,
302
+ "Great for busy storefronts with several connected devices.",
303
+ { list: { items: [] } },
304
+ "Internet Only"
305
+ ),
306
+ ],
307
+ },
308
+ };
309
+
310
+ const TabbedMixedTemplate = () => {
311
+ const [activeTab, setActiveTab] = useState("Business Ready");
312
+ return (
313
+ <div style={{ width: "100%", maxWidth: 960 }}>
314
+ <Carousel
315
+ fields={tabbedMixedFields}
316
+ hasProductCards={true}
317
+ hasTestimonialCards={false}
318
+ disclaimerText={
319
+ "Price includes a $5.00/mo credit with AutoPay, option to disable at the next step."
320
+ }
321
+ backgroundColor="bg-white"
322
+ showSwitch={true}
323
+ activeTab={activeTab}
324
+ setActiveTab={setActiveTab}
325
+ tabs={["Business Ready", "Internet Only"]}
326
+ />
327
+ </div>
328
+ );
329
+ };
330
+
331
+ /**
332
+ * Tab-switch coverage: the "Business Ready" tab mixes one gift card with one
333
+ * no-gift card (the no-gift card reserves the gift height so both CTAs align),
334
+ * while the "Internet Only" tab has two no-gift cards (both gift slots collapse
335
+ * and the CTAs sit flush with the description). Switching tabs recalculates the
336
+ * gift-slot height for only the visible cards.
337
+ */
338
+ export const TabbedMixedGiftAlignment: Story = {
339
+ parameters: { layout: "fullscreen" },
340
+ render: () => <TabbedMixedTemplate />,
341
+ };
342
+
343
+ const twoColumnRowGiftFields: carouselFieldsInterface = {
344
+ __typename: "ComponentCarousel",
345
+ sys: { id: "carousel-two-column-row-gift" },
346
+ title: "Two-column rows: per-row gift height",
347
+ subTitle:
348
+ "On mobile (resize below 768px) the giftless top row collapses while the gift row below reserves its own height.",
349
+ twoColumn: true,
350
+ backgroundColor: "white",
351
+ items: {
352
+ items: [
353
+ // Row 1 — both giftless: the gift slot must collapse so these CTAs sit
354
+ // flush with the description (they must NOT borrow row 2's gift height).
355
+ sampleProduct(
356
+ "row-1a",
357
+ "fiber 100 mbps",
358
+ "19",
359
+ false,
360
+ "Reliable support for essentials like email, browsing, and video chat."
361
+ ),
362
+ sampleProduct(
363
+ "row-1b",
364
+ "fiber 300 mbps",
365
+ "34",
366
+ false,
367
+ "Good for most day-to-day internet uses including streaming video."
368
+ ),
369
+ // Row 2 — both have a gift badge: this row reserves its own gift height.
370
+ sampleProduct(
371
+ "row-2a",
372
+ "fiber 1 gig",
373
+ "39",
374
+ false,
375
+ "Boosted speed and capacity for working from a home office.",
376
+ mastercardGift("100")
377
+ ),
378
+ sampleProduct(
379
+ "row-2b",
380
+ "fiber 2 gig",
381
+ "59",
382
+ false,
383
+ "Ultra-fast speeds for large smart homes.",
384
+ mastercardGift("200")
385
+ ),
386
+ ],
387
+ },
388
+ };
389
+
390
+ /**
391
+ * Two-column mobile regression guard: gift-slot height is equalized per row
392
+ * (the cards next to each other), never across the whole tab. Resize below
393
+ * 768px — row 1 (both giftless) collapses so its CTAs sit flush with the
394
+ * description, while row 2 (both with a gift) reserves only its own height.
395
+ */
396
+ export const TwoColumnRowGiftAlignment: Story = {
397
+ parameters: { layout: "fullscreen" },
398
+ args: {
399
+ fields: twoColumnRowGiftFields,
400
+ hasProductCards: true,
401
+ hasTestimonialCards: false,
402
+ disclaimerText: null,
403
+ backgroundColor: "bg-white",
404
+ activeTab: "",
405
+ setActiveTab: () => {},
406
+ tabs: [],
407
+ },
408
+ };
@@ -32,22 +32,34 @@ jest.mock("@shared/contentful/blocks/cards/product-card", () => ({
32
32
  onToggleExpand,
33
33
  isExpanded,
34
34
  enableBusinessSettings,
35
+ giftBadge,
36
+ hasAnyGift,
35
37
  }: any) => (
36
- <div
38
+ <article
37
39
  data-testid="product-card"
38
40
  data-plan={planName}
39
41
  data-speed={speed}
40
42
  data-expanded={isExpanded}
41
43
  data-business={enableBusinessSettings ? "true" : "false"}
42
44
  >
43
- {planName}
45
+ <div data-pc-header={true}>{planName}</div>
46
+ <div data-pc-description={true}>{speed}</div>
47
+ {giftBadge?.length ? (
48
+ <div data-pc-gift={true} data-testid="gift-slot" />
49
+ ) : hasAnyGift ? (
50
+ <div
51
+ data-pc-gift={true}
52
+ data-pc-gift-empty={true}
53
+ data-testid="gift-slot"
54
+ />
55
+ ) : null}
44
56
  <button onClick={onCtaClick} data-testid="product-cta">
45
57
  CTA
46
58
  </button>
47
59
  <button onClick={onToggleExpand} data-testid="product-toggle">
48
60
  Toggle
49
61
  </button>
50
- </div>
62
+ </article>
51
63
  ),
52
64
  }));
53
65
 
@@ -591,6 +603,256 @@ describe("ProductCardCarousel", () => {
591
603
  removeSpy.mockRestore();
592
604
  });
593
605
  });
606
+
607
+ describe("swipe gestures (desktop carousel)", () => {
608
+ const makeCarousel = () => {
609
+ const items = Array.from({ length: 4 }, (_, i) => ({
610
+ ...sampleProduct,
611
+ sys: { id: `prod-${i}` },
612
+ speed: `${100 * (i + 1)} Mbps|Fiber`,
613
+ }));
614
+ const utils = render(
615
+ <ProductCardCarousel fields={createProductFields(items)} />
616
+ );
617
+ // computeMaxIndex relies on offsetWidth (mocked to 900); a resize makes
618
+ // sure maxIndex is computed so nextSlide/prevSlide can move.
619
+ fireEvent(window, new Event("resize"));
620
+ const windowEl = utils.container.querySelector(
621
+ ".select-none"
622
+ ) as HTMLElement;
623
+ const track = utils.container.querySelector(
624
+ '[style*="translateX"]'
625
+ ) as HTMLElement;
626
+ return { ...utils, windowEl, track };
627
+ };
628
+
629
+ it("advances to the next slide when swiped left past the threshold", () => {
630
+ const { windowEl, track } = makeCarousel();
631
+ expect(screen.queryByTestId("icon-arrow_back")).toBeNull();
632
+
633
+ fireEvent.touchStart(windowEl, { touches: [{ clientX: 500 }] });
634
+ fireEvent.touchMove(windowEl, { touches: [{ clientX: 300 }] });
635
+ // The track follows the finger while dragging (diff = -200).
636
+ expect(track.getAttribute("style")).toContain("-200px");
637
+
638
+ fireEvent.touchEnd(windowEl);
639
+ // nextSlide advanced the index, so the Previous arrow now shows.
640
+ expect(screen.queryByTestId("icon-arrow_back")).not.toBeNull();
641
+ });
642
+
643
+ it("returns to the previous slide when swiped right past the threshold", () => {
644
+ const { windowEl } = makeCarousel();
645
+ // Move forward first so there is a previous slide to return to.
646
+ fireEvent.touchStart(windowEl, { touches: [{ clientX: 500 }] });
647
+ fireEvent.touchMove(windowEl, { touches: [{ clientX: 300 }] });
648
+ fireEvent.touchEnd(windowEl);
649
+ expect(screen.queryByTestId("icon-arrow_back")).not.toBeNull();
650
+
651
+ fireEvent.touchStart(windowEl, { touches: [{ clientX: 300 }] });
652
+ fireEvent.touchMove(windowEl, { touches: [{ clientX: 600 }] });
653
+ fireEvent.touchEnd(windowEl);
654
+ // prevSlide returned to the first slide, hiding the Previous arrow.
655
+ expect(screen.queryByTestId("icon-arrow_back")).toBeNull();
656
+ });
657
+
658
+ it("snaps back without changing slides when the swipe is below the threshold", () => {
659
+ const { windowEl, track } = makeCarousel();
660
+ fireEvent.touchStart(windowEl, { touches: [{ clientX: 500 }] });
661
+ fireEvent.touchMove(windowEl, { touches: [{ clientX: 450 }] }); // diff -50
662
+ fireEvent.touchEnd(windowEl);
663
+ expect(screen.queryByTestId("icon-arrow_back")).toBeNull();
664
+ // Offset is reset to 0 after release.
665
+ expect(track.getAttribute("style")).toContain("+ 0px");
666
+ });
667
+
668
+ it("applies drag resistance at the first-slide boundary", () => {
669
+ const { windowEl, track } = makeCarousel();
670
+ // Dragging right while already at the first slide is damped (diff * 0.3).
671
+ fireEvent.touchStart(windowEl, { touches: [{ clientX: 100 }] });
672
+ fireEvent.touchMove(windowEl, { touches: [{ clientX: 300 }] }); // diff 200
673
+ expect(track.getAttribute("style")).toContain("60px");
674
+ });
675
+
676
+ it("ignores a touch move that is not preceded by a touch start", () => {
677
+ const { windowEl, track } = makeCarousel();
678
+ fireEvent.touchMove(windowEl, { touches: [{ clientX: 300 }] });
679
+ // No swipe offset is applied.
680
+ expect(track.getAttribute("style")).toContain("+ 0px");
681
+ });
682
+ });
683
+
684
+ describe("gift-slot per-row equalization", () => {
685
+ const mockRect = (el: Element, rect: { top?: number; height?: number }) => {
686
+ (el as HTMLElement).getBoundingClientRect = () =>
687
+ ({
688
+ top: rect.top ?? 0,
689
+ left: 0,
690
+ right: 0,
691
+ bottom: (rect.top ?? 0) + (rect.height ?? 0),
692
+ width: 0,
693
+ height: rect.height ?? 0,
694
+ x: 0,
695
+ y: 0,
696
+ toJSON: () => ({}),
697
+ }) as DOMRect;
698
+ };
699
+
700
+ const makeGrid = (
701
+ config: { gift: boolean; top: number; height: number }[]
702
+ ) => {
703
+ const items = config.map((c, i) => ({
704
+ ...sampleProduct,
705
+ sys: { id: `g-${i}` },
706
+ giftRewards: { list: { items: c.gift ? [{ title: "$100" }] : [] } },
707
+ }));
708
+ const { container } = render(
709
+ <ProductCardCarousel fields={createProductFields(items)} />
710
+ );
711
+ const grid = container.querySelector(".grid.grid-cols-2") as HTMLElement;
712
+ const articles = Array.from(grid.querySelectorAll("article"));
713
+ const slots = articles.map(
714
+ a => a.querySelector("[data-pc-gift]") as HTMLElement
715
+ );
716
+ articles.forEach((a, i) => mockRect(a, { top: config[i].top }));
717
+ slots.forEach((s, i) => {
718
+ mockRect(s, { height: config[i].height });
719
+ Object.defineProperty(s, "offsetParent", {
720
+ configurable: true,
721
+ get: () => document.body,
722
+ });
723
+ });
724
+ return { slots };
725
+ };
726
+
727
+ it("collapses a giftless row and never borrows another row's gift height", () => {
728
+ // Row 1 (both giftless) above Row 2 (both with a gift).
729
+ const { slots } = makeGrid([
730
+ { gift: false, top: 0, height: 20 },
731
+ { gift: false, top: 0, height: 20 },
732
+ { gift: true, top: 500, height: 76 },
733
+ { gift: true, top: 500, height: 76 },
734
+ ]);
735
+
736
+ fireEvent(window, new Event("resize"));
737
+
738
+ // Row 1 collapses instead of reserving the gift height.
739
+ expect(slots[0].style.display).toBe("none");
740
+ expect(slots[1].style.display).toBe("none");
741
+ expect(slots[0].style.minHeight).toBe("");
742
+ // Row 2 reserves only its own gift height.
743
+ expect(slots[2].style.minHeight).toBe("76px");
744
+ expect(slots[3].style.minHeight).toBe("76px");
745
+ });
746
+
747
+ it("reserves the tallest gift height within a mixed row so CTAs align", () => {
748
+ // Row 1 = gift + giftless; Row 2 = gift + gift.
749
+ const { slots } = makeGrid([
750
+ { gift: true, top: 0, height: 76 },
751
+ { gift: false, top: 0, height: 20 },
752
+ { gift: true, top: 500, height: 76 },
753
+ { gift: true, top: 500, height: 76 },
754
+ ]);
755
+
756
+ fireEvent(window, new Event("resize"));
757
+
758
+ // The giftless neighbour reserves the taller gift height (CTAs align).
759
+ expect(slots[0].style.minHeight).toBe("76px");
760
+ expect(slots[1].style.minHeight).toBe("76px");
761
+ expect(slots[1].style.display).not.toBe("none");
762
+ });
763
+ });
764
+
765
+ describe("two-column mobile section equalization", () => {
766
+ let originalMatchMedia: typeof window.matchMedia;
767
+
768
+ const setMatchMedia = (matches: boolean) => {
769
+ window.matchMedia = ((query: string) => ({
770
+ matches,
771
+ media: query,
772
+ onchange: null,
773
+ addListener: jest.fn(),
774
+ removeListener: jest.fn(),
775
+ addEventListener: jest.fn(),
776
+ removeEventListener: jest.fn(),
777
+ dispatchEvent: jest.fn(),
778
+ })) as any;
779
+ };
780
+
781
+ const mockHeight = (el: Element, height: number) => {
782
+ (el as HTMLElement).getBoundingClientRect = () =>
783
+ ({
784
+ top: 0,
785
+ left: 0,
786
+ right: 0,
787
+ bottom: height,
788
+ width: 0,
789
+ height,
790
+ x: 0,
791
+ y: 0,
792
+ toJSON: () => ({}),
793
+ }) as DOMRect;
794
+ };
795
+
796
+ beforeEach(() => {
797
+ originalMatchMedia = window.matchMedia;
798
+ });
799
+
800
+ afterEach(() => {
801
+ window.matchMedia = originalMatchMedia;
802
+ });
803
+
804
+ it("matches header and description heights within each row (pair)", () => {
805
+ setMatchMedia(true);
806
+ const items = Array.from({ length: 4 }, (_, i) => ({
807
+ ...sampleProduct,
808
+ sys: { id: `prod-${i}` },
809
+ }));
810
+ const { container } = render(
811
+ <ProductCardCarousel fields={createProductFields(items)} />
812
+ );
813
+ const grid = container.querySelector(".grid.grid-cols-2") as HTMLElement;
814
+ const headers = Array.from(
815
+ grid.querySelectorAll<HTMLElement>("[data-pc-header]")
816
+ );
817
+ const descriptions = Array.from(
818
+ grid.querySelectorAll<HTMLElement>("[data-pc-description]")
819
+ );
820
+ // Row 1 header heights 30/50 -> 50; Row 2 header heights 40/20 -> 40.
821
+ [30, 50, 40, 20].forEach((h, i) => mockHeight(headers[i], h));
822
+ // Row 1 descriptions 25/25 -> 25; Row 2 descriptions 10/60 -> 60.
823
+ [25, 25, 10, 60].forEach((h, i) => mockHeight(descriptions[i], h));
824
+
825
+ fireEvent(window, new Event("resize"));
826
+
827
+ expect(headers[0].style.minHeight).toBe("50px");
828
+ expect(headers[1].style.minHeight).toBe("50px");
829
+ expect(headers[2].style.minHeight).toBe("40px");
830
+ expect(headers[3].style.minHeight).toBe("40px");
831
+
832
+ expect(descriptions[0].style.minHeight).toBe("25px");
833
+ expect(descriptions[1].style.minHeight).toBe("25px");
834
+ expect(descriptions[2].style.minHeight).toBe("60px");
835
+ expect(descriptions[3].style.minHeight).toBe("60px");
836
+ });
837
+
838
+ it("clears section min-heights above the mobile breakpoint", () => {
839
+ setMatchMedia(false);
840
+ const items = Array.from({ length: 4 }, (_, i) => ({
841
+ ...sampleProduct,
842
+ sys: { id: `prod-${i}` },
843
+ }));
844
+ const { container } = render(
845
+ <ProductCardCarousel fields={createProductFields(items)} />
846
+ );
847
+ const grid = container.querySelector(".grid.grid-cols-2") as HTMLElement;
848
+ const header = grid.querySelector<HTMLElement>("[data-pc-header]")!;
849
+ header.style.setProperty("min-height", "123px");
850
+
851
+ fireEvent(window, new Event("resize"));
852
+
853
+ expect(header.style.minHeight).toBe("");
854
+ });
855
+ });
594
856
  });
595
857
 
596
858
  describe("TestimonialCarousel", () => {