@windstream/react-shared-components 0.2.38 → 0.2.41

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 (32) hide show
  1. package/dist/contentful/index.d.ts +1 -2
  2. package/dist/contentful/index.esm.js +2 -2
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +2 -2
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +1 -1
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.esm.js +1 -1
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/styles.css +1 -1
  13. package/package.json +1 -1
  14. package/src/components/brand-button/index.tsx +4 -4
  15. package/src/contentful/blocks/button/Button.stories.tsx +0 -1
  16. package/src/contentful/blocks/button/index.test.tsx +109 -8
  17. package/src/contentful/blocks/button/index.tsx +64 -18
  18. package/src/contentful/blocks/button/types.ts +1 -1
  19. package/src/contentful/blocks/cards/blog-card/index.test.tsx +18 -42
  20. package/src/contentful/blocks/cards/blog-card/index.tsx +45 -63
  21. package/src/contentful/blocks/cards/blog-card/types.ts +0 -1
  22. package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +69 -2
  23. package/src/contentful/blocks/cards/floating-image-card/index.tsx +7 -4
  24. package/src/contentful/blocks/carousel/helper.test.tsx +33 -10
  25. package/src/contentful/blocks/carousel/index.test.tsx +6 -8
  26. package/src/contentful/blocks/carousel/types.test.ts +10 -0
  27. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +0 -1
  28. package/src/contentful/blocks/dynamic-tabs/ordered-tab-views.tsx +1 -1
  29. package/src/contentful/blocks/ordered-list/index.tsx +2 -2
  30. package/src/contentful/blocks/primary-hero/index.tsx +6 -2
  31. package/src/hooks/use-carousel-swipe.test.ts +26 -6
  32. package/src/types/micro-components.ts +2 -2
@@ -9,10 +9,17 @@ import { BrandButton, ButtonVariantsT } from "@shared/components/brand-button";
9
9
  import { Button as CoreButton } from "@shared/components/button";
10
10
  import { Link } from "@shared/components/link";
11
11
  import { MaterialIcon } from "@shared/components/material-icon";
12
+ import type { IconName } from "@shared/components/material-icon/types";
12
13
  import { Text } from "@shared/components/text";
13
14
  import { cx } from "@shared/utils";
14
15
  import { buildPreservedQueryHref } from "@shared/utils/utm";
15
16
 
17
+ const textVariantClasses: Record<string, string> = {
18
+ primary_brand: "text-text-brand",
19
+ primary_inverse: "text-text",
20
+ secondary: "text-text",
21
+ };
22
+
16
23
  export const Button: React.FC<ButtonProps> = props => {
17
24
  const {
18
25
  showButtonAs = "solid",
@@ -33,8 +40,9 @@ export const Button: React.FC<ButtonProps> = props => {
33
40
  idNameToOpen,
34
41
  onModalButtonClick,
35
42
  onClick,
36
- iconName,
37
43
  iconFill = 0,
44
+ buttonIconName,
45
+ buttonIconPosition,
38
46
  size,
39
47
  iconSize = 24,
40
48
  preserveQueryParameters,
@@ -42,6 +50,13 @@ export const Button: React.FC<ButtonProps> = props => {
42
50
  animationType = DEFAULT_BUTTON_ANIMATION_TYPE,
43
51
  ...rest
44
52
  } = props;
53
+
54
+ const isLeftIcon = buttonIconPosition === "left";
55
+ const matIconName = buttonIconName as IconName | undefined;
56
+ const brandButtonClassName = cx(
57
+ buttonClassName,
58
+ isLeftIcon && matIconName ? "flex-row-reverse" : ""
59
+ );
45
60
 
46
61
  if (
47
62
  preDefinedFunctionExecution === "check availability" &&
@@ -116,7 +131,6 @@ export const Button: React.FC<ButtonProps> = props => {
116
131
  : href && preserveQueryParameters && currentSearch
117
132
  ? buildPreservedQueryHref(href, currentSearch)
118
133
  : href;
119
-
120
134
  let CTA = (
121
135
  <CoreButton onClick={buttonClick} className={buttonClassName} {...rest} />
122
136
  );
@@ -132,9 +146,12 @@ export const Button: React.FC<ButtonProps> = props => {
132
146
  text={buttonLabel}
133
147
  label={buttonPrefix}
134
148
  fullWidth={fullWidth}
135
- buttonClassName={buttonClassName}
149
+ buttonClassName={brandButtonClassName}
136
150
  size={size}
137
151
  onClick={linkClick}
152
+ iconName={matIconName}
153
+ iconSize={iconSize}
154
+ iconFill={iconFill}
138
155
  disableAnimation={disableAnimation}
139
156
  animationType={animationType}
140
157
  />
@@ -147,7 +164,10 @@ export const Button: React.FC<ButtonProps> = props => {
147
164
  fullWidth={fullWidth}
148
165
  size={size}
149
166
  onClick={buttonClick}
150
- buttonClassName={buttonClassName}
167
+ iconName={matIconName}
168
+ iconSize={iconSize}
169
+ iconFill={iconFill}
170
+ buttonClassName={brandButtonClassName}
151
171
  disableAnimation={disableAnimation}
152
172
  animationType={animationType}
153
173
  />
@@ -160,16 +180,34 @@ export const Button: React.FC<ButtonProps> = props => {
160
180
  href={resolvedHref}
161
181
  target={target}
162
182
  variant={linkVariant || "default"}
163
- className={`text-text-brand ${linkClassName || ""}`}
183
+ className={cx(
184
+ matIconName &&
185
+ "label1 inline-flex items-center gap-2 no-underline",
186
+ textVariantClasses[buttonVariant],
187
+ linkClassName
188
+ )}
164
189
  onClick={linkClick}
165
190
  >
166
- {buttonLabel}{" "}
167
- {iconName && (
168
- <MaterialIcon
169
- name={iconName as any}
170
- size={iconSize}
171
- fill={iconFill}
172
- />
191
+ {matIconName ? (
192
+ <>
193
+ {isLeftIcon ? (
194
+ <MaterialIcon
195
+ name={matIconName as IconName}
196
+ size={iconSize}
197
+ fill={iconFill}
198
+ />
199
+ ) : null}
200
+ {buttonLabel}
201
+ {!isLeftIcon ? (
202
+ <MaterialIcon
203
+ name={matIconName as IconName}
204
+ size={iconSize}
205
+ fill={iconFill}
206
+ />
207
+ ) : null}
208
+ </>
209
+ ) : (
210
+ <>{buttonLabel}</>
173
211
  )}
174
212
  </Link>
175
213
  );
@@ -184,10 +222,10 @@ export const Button: React.FC<ButtonProps> = props => {
184
222
  text={buttonLabel}
185
223
  label={buttonPrefix}
186
224
  fullWidth={fullWidth}
187
- buttonClassName={buttonClassName}
225
+ buttonClassName={brandButtonClassName}
188
226
  size={size}
189
227
  onClick={linkClick}
190
- iconName={iconName as any}
228
+ iconName={matIconName}
191
229
  iconSize={iconSize}
192
230
  iconFill={iconFill}
193
231
  disableAnimation={disableAnimation}
@@ -200,10 +238,10 @@ export const Button: React.FC<ButtonProps> = props => {
200
238
  text={buttonLabel}
201
239
  label={buttonPrefix}
202
240
  fullWidth={fullWidth}
203
- buttonClassName={buttonClassName}
241
+ buttonClassName={brandButtonClassName}
204
242
  size={size}
205
243
  onClick={buttonClick}
206
- iconName={iconName as any}
244
+ iconName={matIconName}
207
245
  iconSize={iconSize}
208
246
  iconFill={iconFill}
209
247
  disableAnimation={disableAnimation}
@@ -237,12 +275,20 @@ export const Button: React.FC<ButtonProps> = props => {
237
275
  )}
238
276
  aria-label={buttonLabel}
239
277
  >
278
+ {isLeftIcon && matIconName ? (
279
+ <MaterialIcon
280
+ name={matIconName as IconName}
281
+ fill={iconFill}
282
+ size={iconSize}
283
+ weight="200"
284
+ />
285
+ ) : null}
240
286
  <Text aria-label={buttonLabel} className="label1 text-nowrap">
241
287
  {buttonLabel}
242
288
  </Text>
243
- {iconName && (
289
+ {matIconName && !isLeftIcon && (
244
290
  <MaterialIcon
245
- name={iconName as any}
291
+ name={matIconName as IconName}
246
292
  fill={iconFill}
247
293
  size={iconSize}
248
294
  weight="200"
@@ -17,7 +17,7 @@ export type ButtonProps = {
17
17
  buttonPrefix?: string;
18
18
  badge?: string;
19
19
  badgeIcon?: string;
20
- buttonIcon?: string;
20
+ buttonIconName?: string;
21
21
  buttonIconPosition?: "left" | "right";
22
22
  href?: string;
23
23
  target?: "_self" | "_blank";
@@ -14,14 +14,6 @@ jest.mock("next/link", () => {
14
14
  return MockLink;
15
15
  });
16
16
 
17
- jest.mock("@shared/components/material-icon", () => ({
18
- MaterialIcon: ({ name }: any) => (
19
- <span data-testid="material-icon" data-name={name}>
20
- {name}
21
- </span>
22
- ),
23
- }));
24
-
25
17
  jest.mock("@shared/components/text", () => ({
26
18
  Text: ({ children, className, ...rest }: any) => (
27
19
  <span className={className} {...rest}>
@@ -74,16 +66,6 @@ describe("BlogCard", () => {
74
66
  expect(screen.getByText("•")).toBeInTheDocument();
75
67
  });
76
68
 
77
- it("renders read more link", () => {
78
- render(<BlogCard {...defaultProps} />);
79
- expect(screen.getByText("Read more")).toBeInTheDocument();
80
- });
81
-
82
- it("renders custom readMoreText", () => {
83
- render(<BlogCard {...defaultProps} readMoreText="Continue reading" />);
84
- expect(screen.getByText("Continue reading")).toBeInTheDocument();
85
- });
86
-
87
69
  it("does not render date separator when date is not provided", () => {
88
70
  render(<BlogCard {...defaultProps} date={undefined} />);
89
71
  expect(screen.queryByText("•")).not.toBeInTheDocument();
@@ -171,6 +153,24 @@ describe("BlogCard", () => {
171
153
  });
172
154
  });
173
155
 
156
+ describe("Navigation", () => {
157
+ it("renders a link to href when title is missing", () => {
158
+ const { container } = render(
159
+ <BlogCard {...defaultProps} title={undefined} />
160
+ );
161
+ const links = container.querySelectorAll("a");
162
+ expect(links).toHaveLength(1);
163
+ expect(links[0]).toHaveAttribute("href", "/blog/test-post");
164
+ });
165
+
166
+ it("renders exactly one link when title is present", () => {
167
+ const { container } = render(<BlogCard {...defaultProps} />);
168
+ const links = container.querySelectorAll("a");
169
+ expect(links).toHaveLength(1);
170
+ expect(links[0]).toHaveAttribute("href", "/blog/test-post");
171
+ });
172
+ });
173
+
174
174
  describe("Optional props", () => {
175
175
  it("renders without title", () => {
176
176
  const { container } = render(
@@ -185,29 +185,5 @@ describe("BlogCard", () => {
185
185
  );
186
186
  expect(container.querySelector("article")).toBeInTheDocument();
187
187
  });
188
-
189
- it("sets aria-label on read more link", () => {
190
- render(<BlogCard {...defaultProps} />);
191
- const links = screen.getAllByRole("link");
192
- const readMoreLink = links.find(link =>
193
- link.getAttribute("aria-label")?.includes("Read more")
194
- );
195
- expect(readMoreLink).toHaveAttribute(
196
- "aria-label",
197
- "Read more about Test Blog Post"
198
- );
199
- });
200
-
201
- it("uses fallback aria-label when title is not provided", () => {
202
- render(<BlogCard {...defaultProps} title={undefined} />);
203
- const links = screen.getAllByRole("link");
204
- const readMoreLink = links.find(link =>
205
- link.getAttribute("aria-label")?.includes("Read more")
206
- );
207
- expect(readMoreLink).toHaveAttribute(
208
- "aria-label",
209
- "Read more about this article"
210
- );
211
- });
212
188
  });
213
189
  });
@@ -2,8 +2,6 @@ import React from "react";
2
2
  import { BlogCardProps } from "./types";
3
3
  import * as _NextLink from "next/link";
4
4
 
5
- // import { Button } from "@shared/components/button";
6
- import { MaterialIcon } from "@shared/components/material-icon";
7
5
  import { Text } from "@shared/components/text";
8
6
 
9
7
  // CJS/ESM interop: next/link uses standard __esModule exports so this is safe.
@@ -21,13 +19,12 @@ export const BlogCard: React.FC<BlogCardProps> = ({
21
19
  category,
22
20
  image,
23
21
  imageComponent: ImageComponent,
24
- readMoreText = "Read more",
25
22
  asGrid = true,
26
23
  index,
27
24
  }: BlogCardProps) => {
28
25
  const parentClassName = asGrid
29
- ? "flex h-full flex-col overflow-hidden rounded-card-lg bg-white shadow-drop transition-all duration-200 hover:-translate-y-0.5 hover:shadow-cardDrop"
30
- : `callout-card w-full flex h-full flex-col overflow-hidden`;
26
+ ? "group relative flex h-full flex-col overflow-hidden rounded-card-lg bg-white shadow-drop transition-all duration-200 hover:-translate-y-0.5 hover:shadow-cardDrop"
27
+ : `callout-card group relative w-full flex h-full flex-col overflow-hidden`;
31
28
 
32
29
  return (
33
30
  <article
@@ -38,42 +35,40 @@ export const BlogCard: React.FC<BlogCardProps> = ({
38
35
  >
39
36
  {/* Image (hidden in list view) */}
40
37
  {asGrid && (
41
- <Link href={href} tabIndex={-1} aria-hidden="true" className="block">
42
- <div className="h-[232px] w-full flex-shrink-0 overflow-hidden bg-gray-100">
43
- {image ? (
44
- ImageComponent ? (
45
- <ImageComponent
46
- src={image.src}
47
- alt={image.alt}
48
- width={image.width}
49
- height={image.height}
50
- className="h-full w-full object-cover transition-transform duration-300 hover:scale-[1.03]"
51
- sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
52
- />
53
- ) : (
54
- <img
55
- src={image.src}
56
- alt={image.alt}
57
- width={image.width}
58
- height={image.height}
59
- loading="lazy"
60
- decoding="async"
61
- className="h-full w-full object-cover transition-transform duration-300 hover:scale-[1.03]"
62
- />
63
- )
38
+ <div className="block h-[232px] w-full flex-shrink-0 overflow-hidden bg-gray-100">
39
+ {image ? (
40
+ ImageComponent ? (
41
+ <ImageComponent
42
+ src={image.src}
43
+ alt={image.alt}
44
+ width={image.width}
45
+ height={image.height}
46
+ className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
47
+ sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
48
+ />
64
49
  ) : (
65
- <div
66
- className="h-full w-full bg-gradient-to-br from-gray-200 to-gray-100"
67
- aria-hidden="true"
50
+ <img
51
+ src={image.src}
52
+ alt={image.alt}
53
+ width={image.width}
54
+ height={image.height}
55
+ loading="lazy"
56
+ decoding="async"
57
+ className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
68
58
  />
69
- )}
70
- </div>
71
- </Link>
59
+ )
60
+ ) : (
61
+ <div
62
+ className="h-full w-full bg-gradient-to-br from-gray-200 to-gray-100"
63
+ aria-hidden="true"
64
+ />
65
+ )}
66
+ </div>
72
67
  )}
73
68
 
74
69
  {/* Body */}
75
70
  <div
76
- className={`flex flex-1 flex-col ${
71
+ className={`flex flex-1 flex-col ${
77
72
  asGrid ? "gap-5 p-6 md:p-8" : "gap-1 border-b p-3 md:p-4"
78
73
  }`}
79
74
  >
@@ -94,12 +89,14 @@ export const BlogCard: React.FC<BlogCardProps> = ({
94
89
 
95
90
  {/* Title */}
96
91
  {title && (
97
- <Link
98
- href={href}
99
- className="heading6 m-0 line-clamp-3 font-black transition-colors duration-150 hover:text-text-brand"
100
- >
101
- {title}
102
- </Link>
92
+ <Text as="h2" className="heading6">
93
+ <Link
94
+ href={href}
95
+ className="m-0 line-clamp-3 font-black transition-colors duration-150 after:absolute after:inset-0 after:z-10 after:content-[''] hover:text-text-brand group-hover:text-text-brand"
96
+ >
97
+ {title}
98
+ </Link>
99
+ </Text>
103
100
  )}
104
101
 
105
102
  {/* Excerpt */}
@@ -108,29 +105,14 @@ export const BlogCard: React.FC<BlogCardProps> = ({
108
105
  {description}
109
106
  </Text>
110
107
  )}
111
-
112
- {/* Read more (hidden in list view) */}
113
- {asGrid && (
114
- <Link
115
- href={href}
116
- className="group mt-auto inline-flex items-center justify-start gap-2 pt-3 text-sm text-text-brand no-underline"
117
- aria-label={`${readMoreText} about ${title || "this article"}`}
118
- >
119
- <Text
120
- aria-label={`${readMoreText} about ${title || "this article"}`}
121
- className="label1 text-nowrap"
122
- >
123
- {readMoreText}
124
- </Text>
125
- <MaterialIcon
126
- name="expand_circle_right"
127
- fill={1}
128
- size={24}
129
- weight="200"
130
- />
131
- </Link>
132
- )}
133
108
  </div>
109
+
110
+ {/* Fallback stretched link keeps the card navigable when no title link is rendered */}
111
+ {!title && (
112
+ <Link href={href} className="absolute inset-0 z-10">
113
+ <span className="sr-only">{category ?? "Read more"}</span>
114
+ </Link>
115
+ )}
134
116
  </article>
135
117
  );
136
118
  };
@@ -26,7 +26,6 @@ export interface BlogCardProps {
26
26
  };
27
27
  /** Pass next/image's Image component here to enable image optimization */
28
28
  imageComponent?: React.ComponentType<BlogCardImageProps>;
29
- readMoreText?: string;
30
29
  asGrid?: boolean;
31
30
  index?: number;
32
31
  }
@@ -1,10 +1,12 @@
1
1
  import React from "react";
2
2
  import { FloatingImageCard } from "./index";
3
3
 
4
+ import { describe, expect, it, jest } from "@jest/globals";
5
+ import "@testing-library/jest-dom/jest-globals";
4
6
  import { render, screen } from "@testing-library/react";
5
7
 
6
8
  jest.mock("@shared/components/next-image", () => ({
7
- NextImage: ({ src, alt, width, height, className, onClick }: any) => (
9
+ NextImage: ({ src, alt, width, height, className, onClick, ...rest }: any) => (
8
10
  <img
9
11
  data-testid="next-image"
10
12
  src={src}
@@ -13,6 +15,7 @@ jest.mock("@shared/components/next-image", () => ({
13
15
  height={height}
14
16
  className={className}
15
17
  onClick={onClick}
18
+ {...rest}
16
19
  />
17
20
  ),
18
21
  }));
@@ -98,6 +101,68 @@ describe("FloatingImageCard", () => {
98
101
  render(<FloatingImageCard card={defaultCard} />);
99
102
  expect(screen.queryByTestId("cta-button")).not.toBeInTheDocument();
100
103
  });
104
+
105
+ it("renders Button when buttonIconName is provided", () => {
106
+ const card = {
107
+ ...defaultCard,
108
+ cta: {
109
+ buttonLabel: "See Jobs",
110
+ href: "/jobs",
111
+ showButtonAs: "text" as const,
112
+ buttonIconName: "expand_circle_right",
113
+ buttonIconPosition: "left" as const,
114
+ },
115
+ };
116
+
117
+ render(<FloatingImageCard card={card} />);
118
+ expect(screen.getByTestId("cta-button")).toBeInTheDocument();
119
+ });
120
+
121
+ it("renders Button for primary_inverse text CTA variant", () => {
122
+ const card = {
123
+ ...defaultCard,
124
+ cta: {
125
+ buttonLabel: "See Jobs",
126
+ href: "/jobs",
127
+ showButtonAs: "text" as const,
128
+ buttonVariant: "primary_inverse" as const,
129
+ buttonIconName: "expand_circle_right",
130
+ },
131
+ };
132
+
133
+ render(<FloatingImageCard card={card} />);
134
+ expect(screen.getByTestId("cta-button")).toBeInTheDocument();
135
+ });
136
+
137
+ it("renders Button for primary_navy alias", () => {
138
+ const card = {
139
+ ...defaultCard,
140
+ cta: {
141
+ buttonLabel: "See Jobs",
142
+ href: "/jobs",
143
+ showButtonAs: "text" as const,
144
+ buttonVariant: "primary_navy" as any,
145
+ buttonIconName: "expand_circle_right",
146
+ },
147
+ };
148
+
149
+ render(<FloatingImageCard card={card} />);
150
+ expect(screen.getByTestId("cta-button")).toBeInTheDocument();
151
+ });
152
+
153
+ it("renders authored icon CTA when buttonIconName is set even if href is missing", () => {
154
+ const card = {
155
+ ...defaultCard,
156
+ cta: {
157
+ buttonLabel: "See Jobs",
158
+ showButtonAs: "text" as const,
159
+ buttonIconName: "expand_circle_right",
160
+ },
161
+ };
162
+
163
+ render(<FloatingImageCard card={card} />);
164
+ expect(screen.getByTestId("cta-button")).toBeInTheDocument();
165
+ });
101
166
  });
102
167
 
103
168
  describe("CTA alignment", () => {
@@ -158,7 +223,9 @@ describe("FloatingImageCard", () => {
158
223
 
159
224
  describe("Image link", () => {
160
225
  it("makes image clickable when imageLink is provided", () => {
161
- const windowOpen = jest.spyOn(window, "open").mockImplementation();
226
+ const windowOpen = jest
227
+ .spyOn(window, "open")
228
+ .mockImplementation(() => null);
162
229
  const card = {
163
230
  ...defaultCard,
164
231
  imageLink: "https://example.com",
@@ -106,11 +106,14 @@ export const FloatingImageCard: React.FC<FloatingImageCardProps> = ({
106
106
  <div className={cx(ctaAlignmentClass, "mt-auto")}>
107
107
  {cta ? (
108
108
  <Button
109
- linkVariant="unstyled"
110
- linkClassName="label1 flex items-center gap-2 text-text"
111
109
  {...cta}
112
- iconName="expand_circle_right"
113
- iconFill={1}
110
+ showButtonAs={
111
+ cta.showButtonAs || (cta.buttonIconName ? undefined : "text-with-icon")
112
+ }
113
+ iconName={
114
+ cta.iconName || (cta.buttonIconName ? undefined : "expand_circle_right")
115
+ }
116
+ iconFill={cta.iconFill ?? 1}
114
117
  // Button animates ONLY when BOTH are explicitly enabled:
115
118
  // - the parent Callout animation is enabled (disableAnimation === false)
116
119
  // - the button's own disableAnimation is enabled (=== false)
@@ -7,7 +7,7 @@ import {
7
7
  CarouselWithTestimonialCards,
8
8
  } from "./types";
9
9
 
10
- import { fireEvent, render, screen } from "@testing-library/react";
10
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
11
11
 
12
12
  // Mock sub-components
13
13
  jest.mock("@shared/components/button", () => ({
@@ -84,6 +84,12 @@ jest.mock("@shared/utils", () => ({
84
84
  }));
85
85
 
86
86
  describe("ProductCardCarousel", () => {
87
+ beforeEach(() => {
88
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
89
+ configurable: true,
90
+ get: () => 900,
91
+ });
92
+ });
87
93
  const createProductFields = (
88
94
  items: any[] = []
89
95
  ): CarouselWithProductCards => ({
@@ -208,39 +214,56 @@ describe("ProductCardCarousel", () => {
208
214
  expect(card).toHaveAttribute("data-plan", "500 Mbps");
209
215
  });
210
216
 
211
- it("renders navigation arrows for carousel mode", () => {
217
+ it("renders Next arrow and hides Previous on first slide in carousel mode", async () => {
212
218
  const items = Array.from({ length: 4 }, (_, i) => ({
213
219
  ...sampleProduct,
214
220
  sys: { id: `prod-${i}` },
215
221
  }));
216
222
  const fields = createProductFields(items);
217
223
  render(<ProductCardCarousel fields={fields} />);
218
- expect(screen.getByLabelText("Previous")).toBeInTheDocument();
219
- expect(screen.getByLabelText("Next")).toBeInTheDocument();
224
+
225
+ fireEvent(window, new Event("resize"));
226
+
227
+ await waitFor(() =>
228
+ expect(screen.getByLabelText("Next")).toBeInTheDocument()
229
+ );
230
+ expect(screen.queryByLabelText("Previous")).not.toBeInTheDocument();
220
231
  });
221
232
 
222
- it("calls prevSlide on Previous button click", () => {
233
+ it("shows Previous after moving forward, then hides it after returning to first slide", async () => {
223
234
  const items = Array.from({ length: 4 }, (_, i) => ({
224
235
  ...sampleProduct,
225
236
  sys: { id: `prod-${i}` },
226
237
  }));
227
238
  const fields = createProductFields(items);
228
239
  render(<ProductCardCarousel fields={fields} />);
229
- const prevBtn = screen.getByLabelText("Previous");
230
- fireEvent.click(prevBtn);
231
- // No error thrown, action processed
240
+
241
+ fireEvent(window, new Event("resize"));
242
+
243
+ const nextBtn = await screen.findByLabelText("Next");
244
+ fireEvent.click(nextBtn);
245
+
246
+ const prevBtn = await screen.findByLabelText("Previous");
232
247
  expect(prevBtn).toBeInTheDocument();
248
+
249
+ fireEvent.click(prevBtn);
250
+
251
+ expect(screen.queryByLabelText("Previous")).not.toBeInTheDocument();
233
252
  });
234
253
 
235
- it("calls nextSlide on Next button click", () => {
254
+ it("calls nextSlide on Next button click", async () => {
236
255
  const items = Array.from({ length: 4 }, (_, i) => ({
237
256
  ...sampleProduct,
238
257
  sys: { id: `prod-${i}` },
239
258
  }));
240
259
  const fields = createProductFields(items);
241
260
  render(<ProductCardCarousel fields={fields} />);
242
- const nextBtn = screen.getByLabelText("Next");
261
+
262
+ fireEvent(window, new Event("resize"));
263
+
264
+ const nextBtn = await screen.findByLabelText("Next");
243
265
  fireEvent.click(nextBtn);
266
+
244
267
  expect(nextBtn).toBeInTheDocument();
245
268
  });
246
269
 
@@ -152,13 +152,12 @@ describe("Carousel", () => {
152
152
  expect(screen.getByTestId("product-card-carousel")).toBeInTheDocument();
153
153
  });
154
154
 
155
- it("does not render ProductCardCarousel when showSwitch is false", () => {
155
+ it("renders ProductCardCarousel when hasProductCards is true even if showSwitch is false", () => {
156
156
  render(
157
157
  <Carousel {...defaultProps} hasProductCards={true} showSwitch={false} />
158
158
  );
159
- expect(
160
- screen.queryByTestId("product-card-carousel")
161
- ).not.toBeInTheDocument();
159
+ expect(screen.getByTestId("product-card-carousel")).toBeInTheDocument();
160
+ expect(screen.queryByTestId("tab-switch")).not.toBeInTheDocument();
162
161
  });
163
162
 
164
163
  it("renders TabSwitch when showSwitch and hasProductCards with multiple tabs", () => {
@@ -279,7 +278,7 @@ describe("Carousel", () => {
279
278
  expect(screen.getByTestId("testimonial-carousel")).toBeInTheDocument();
280
279
  });
281
280
 
282
- it("does not render product carousel when showSwitch is false even with hasProductCards", () => {
281
+ it("renders product carousel and hides tab switch when showSwitch is false with hasProductCards", () => {
283
282
  render(
284
283
  <Carousel
285
284
  {...defaultProps}
@@ -288,9 +287,8 @@ describe("Carousel", () => {
288
287
  hasTestimonialCards={false}
289
288
  />
290
289
  );
291
- expect(
292
- screen.queryByTestId("product-card-carousel")
293
- ).not.toBeInTheDocument();
290
+ expect(screen.getByTestId("product-card-carousel")).toBeInTheDocument();
291
+ expect(screen.queryByTestId("tab-switch")).not.toBeInTheDocument();
294
292
  });
295
293
 
296
294
  it("renders with empty tabs array and no TabSwitch", () => {