@windstream/react-shared-components 0.2.41 → 0.2.43

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 (34) hide show
  1. package/dist/contentful/index.d.ts +1 -1
  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 +3 -3
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/index.esm.js +1 -1
  7. package/dist/index.esm.js.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/next/index.esm.js +1 -1
  11. package/dist/next/index.esm.js.map +1 -1
  12. package/dist/next/index.js +1 -1
  13. package/dist/next/index.js.map +1 -1
  14. package/dist/styles.css +1 -1
  15. package/dist/utils/index.esm.js +1 -1
  16. package/dist/utils/index.esm.js.map +1 -1
  17. package/dist/utils/index.js +1 -1
  18. package/dist/utils/index.js.map +1 -1
  19. package/package.json +1 -1
  20. package/src/components/animation-wrapper/index.test.tsx +6 -6
  21. package/src/components/link/index.test.tsx +2 -2
  22. package/src/components/link/index.tsx +1 -1
  23. package/src/components/video-link/index.test.tsx +43 -4
  24. package/src/components/video-link/index.tsx +32 -4
  25. package/src/components/video-link/types.ts +1 -0
  26. package/src/contentful/blocks/blogs-grid/index.test.tsx +45 -14
  27. package/src/contentful/blocks/email-input-block/index.test.tsx +32 -41
  28. package/src/contentful/blocks/find-kinetic/index.test.tsx +3 -2
  29. package/src/contentful/blocks/image-promo-bar/index.test.tsx +55 -10
  30. package/src/contentful/blocks/image-promo-bar/index.tsx +5 -2
  31. package/src/contentful/blocks/image-promo-bar/types.ts +1 -1
  32. package/src/contentful/blocks/primary-hero/index.tsx +53 -41
  33. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +4 -3
  34. package/src/hooks/contentful/use-contentful-rich-text.tsx +69 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windstream/react-shared-components",
3
- "version": "0.2.41",
3
+ "version": "0.2.43",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -359,7 +359,7 @@ describe("AnimationWrapper", () => {
359
359
  expect(transition.duration).toBe(1);
360
360
  });
361
361
 
362
- it("uses custom whileHover even without animationType", () => {
362
+ it("merges custom whileHover with the default animation preset", () => {
363
363
  render(
364
364
  <AnimationWrapper
365
365
  disableAnimation={false}
@@ -372,10 +372,10 @@ describe("AnimationWrapper", () => {
372
372
  const whileHover = JSON.parse(
373
373
  el.getAttribute("data-while-hover") || "{}"
374
374
  );
375
- expect(whileHover).toEqual({ rotate: 10 });
375
+ expect(whileHover).toEqual({ scale: 1.05, rotate: 10 });
376
376
  });
377
377
 
378
- it("uses custom whileTap without animationType", () => {
378
+ it("merges custom whileTap with the default animation preset", () => {
379
379
  render(
380
380
  <AnimationWrapper disableAnimation={false} whileTap={{ rotate: -5 }}>
381
381
  <div data-testid="child">Tap</div>
@@ -383,10 +383,10 @@ describe("AnimationWrapper", () => {
383
383
  );
384
384
  const el = screen.getByTestId("child");
385
385
  const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}");
386
- expect(whileTap).toEqual({ rotate: -5 });
386
+ expect(whileTap).toEqual({ scale: 0.95, rotate: -5 });
387
387
  });
388
388
 
389
- it("uses custom transition without animationType", () => {
389
+ it("merges custom transition with the default animation preset", () => {
390
390
  render(
391
391
  <AnimationWrapper
392
392
  disableAnimation={false}
@@ -399,7 +399,7 @@ describe("AnimationWrapper", () => {
399
399
  const transition = JSON.parse(
400
400
  el.getAttribute("data-transition") || "{}"
401
401
  );
402
- expect(transition).toEqual({ delay: 0.5 });
402
+ expect(transition).toEqual({ duration: 0.15, delay: 0.5 });
403
403
  });
404
404
  });
405
405
 
@@ -155,14 +155,14 @@ describe("Link", () => {
155
155
  expect(el).toHaveAttribute("target", "_blank");
156
156
  });
157
157
 
158
- it("adds rel=noopener noreferrer for external links", () => {
158
+ it("adds rel=noopener for external links", () => {
159
159
  render(
160
160
  <Link href="https://example.com" external={true}>
161
161
  External
162
162
  </Link>
163
163
  );
164
164
  const el = screen.getByText("External").closest("a");
165
- expect(el).toHaveAttribute("rel", "noopener noreferrer");
165
+ expect(el).toHaveAttribute("rel", "noopener");
166
166
  });
167
167
 
168
168
  it("does not add target for disabled external links", () => {
@@ -88,7 +88,7 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
88
88
  ...(external &&
89
89
  !disabled && {
90
90
  target: "_blank",
91
- rel: "noopener noreferrer",
91
+ rel: "noopener",
92
92
  }),
93
93
  ...(disabled && {
94
94
  "aria-disabled": true,
@@ -55,20 +55,59 @@ describe("VideoLink", () => {
55
55
  expect(container.innerHTML).toBe("");
56
56
  });
57
57
 
58
- it("renders nothing when videoLink has neither link nor videoAssetUrl", () => {
58
+ it("renders nothing when there is no video and no image to fall back to", () => {
59
+ const { container } = render(<VideoLink videoLink={{}} />);
60
+ expect(container.innerHTML).toBe("");
61
+ });
62
+
63
+ it("renders the video link's own poster as a static image when there is no video", () => {
64
+ render(<VideoLink videoLink={{ image: "thumb.jpg" }} />);
65
+ const img = screen.getByAltText("section-image");
66
+ expect(img).toHaveAttribute("src", "thumb.jpg");
67
+ expect(screen.queryByTestId("play-button")).not.toBeInTheDocument();
68
+ });
69
+
70
+ it("ignores an unrelated fallbackImage-like prop - only videoLink.image is used as the poster", () => {
71
+ const { container } = render(
72
+ // @ts-expect-error - fallbackImage is intentionally not a supported prop
73
+ <VideoLink videoLink={{}} fallbackImage="https://img.test/hero.jpg" />
74
+ );
75
+ expect(container.innerHTML).toBe("");
76
+ });
77
+
78
+ it("renders nothing when videoSourceType is 'contentful' but videoAssetUrl is missing and there is no image", () => {
59
79
  const { container } = render(
60
- <VideoLink videoLink={{ image: "thumb.jpg" }} />
80
+ <VideoLink videoLink={{ videoSourceType: "contentful" }} />
61
81
  );
62
82
  expect(container.innerHTML).toBe("");
63
83
  });
64
84
 
65
- it("renders nothing when videoSourceType is 'contentful' but videoAssetUrl is missing", () => {
85
+ it("falls back to videoLink.link when videoSourceType is 'contentful' but videoAssetUrl is missing", () => {
66
86
  const { container } = render(
87
+ <VideoLink
88
+ videoLink={{
89
+ videoSourceType: "contentful",
90
+ link: "https://youtube.com/watch?v=123",
91
+ }}
92
+ />
93
+ );
94
+ clickVideoSection(container);
95
+ expect(screen.getByTestId("youtube-embed")).toHaveAttribute(
96
+ "data-link",
97
+ "https://youtube.com/watch?v=123"
98
+ );
99
+ });
100
+
101
+ it("falls back to the poster when videoSourceType is 'contentful' but videoAssetUrl is missing", () => {
102
+ render(
67
103
  <VideoLink
68
104
  videoLink={{ videoSourceType: "contentful", image: "thumb.jpg" }}
69
105
  />
70
106
  );
71
- expect(container.innerHTML).toBe("");
107
+ expect(screen.getByAltText("section-image")).toHaveAttribute(
108
+ "src",
109
+ "thumb.jpg"
110
+ );
72
111
  });
73
112
 
74
113
  it("renders preview image and play button when videoLink has link and image", () => {
@@ -14,6 +14,7 @@ export const VideoLink: React.FC<VideoLinkComponentProps> = ({
14
14
  imageWidth = 660,
15
15
  imageHeight = 660,
16
16
  useOriginalImageAspectRatio = false,
17
+ isAboveTheFold = false,
17
18
  }) => {
18
19
  const [activeVideo, setActiveVideo] = useState("");
19
20
  const [isHovered, setIsHovered] = useState(false);
@@ -24,8 +25,35 @@ export const VideoLink: React.FC<VideoLinkComponentProps> = ({
24
25
  ? videoLink?.videoAssetUrl
25
26
  : videoLink?.link) ?? "";
26
27
 
28
+ // The video's own poster - never the promo bar's plain image field.
29
+ const posterImage = videoLink?.image;
30
+
27
31
  if (!videoHref) {
28
- return null;
32
+ if (!posterImage) {
33
+ return null;
34
+ }
35
+ return (
36
+ <div
37
+ className={cx(
38
+ !useOriginalImageAspectRatio
39
+ ? "relative aspect-[16/9] w-full overflow-hidden rounded-image lg:h-[486px] xl:aspect-square xl:max-h-[486px] xl:w-[486px]"
40
+ : "overflow-hidden rounded-image"
41
+ )}
42
+ >
43
+ <NextImage
44
+ src={posterImage}
45
+ alt="section-image"
46
+ priority={isAboveTheFold}
47
+ fill={!useOriginalImageAspectRatio}
48
+ width={!useOriginalImageAspectRatio ? undefined : imageWidth || 660}
49
+ height={!useOriginalImageAspectRatio ? undefined : imageHeight || 660}
50
+ sizes="(max-width: 430px) 100vw, (max-width: 834px) 100vw, 50vw"
51
+ className={cx(
52
+ !useOriginalImageAspectRatio ? "object-cover" : "w-full"
53
+ )}
54
+ />
55
+ </div>
56
+ );
29
57
  }
30
58
 
31
59
  const videoPopup = videoLink?.videoPopup ?? false;
@@ -46,7 +74,7 @@ export const VideoLink: React.FC<VideoLinkComponentProps> = ({
46
74
  <ContentfulVideoEmbed
47
75
  videoAssetUrl={videoLink?.videoAssetUrl}
48
76
  videoAssetType={videoLink?.videoAssetType}
49
- image={videoLink?.image}
77
+ image={posterImage}
50
78
  title={videoLink?.title}
51
79
  description={videoLink?.description}
52
80
  autoplay={isPlaying}
@@ -79,9 +107,9 @@ export const VideoLink: React.FC<VideoLinkComponentProps> = ({
79
107
  "relative w-full transition-opacity duration-300"
80
108
  )}
81
109
  >
82
- {videoLink?.image && (
110
+ {posterImage && (
83
111
  <NextImage
84
- src={videoLink.image}
112
+ src={posterImage}
85
113
  alt="Video preview"
86
114
  width={imageWidth || 660}
87
115
  height={imageHeight || 660}
@@ -20,6 +20,7 @@ export type VideoLinkComponentProps = {
20
20
  imageWidth?: number;
21
21
  imageHeight?: number;
22
22
  useOriginalImageAspectRatio?: boolean;
23
+ isAboveTheFold?: boolean;
23
24
  };
24
25
 
25
26
  export type PlayButtonProps = {
@@ -14,17 +14,31 @@ jest.mock("next/navigation", () => ({
14
14
  }));
15
15
 
16
16
  jest.mock("@shared/components/pagination", () => ({
17
- Pagination: ({ currentPage, totalPages, onPageChange, ariaLabel }: any) => (
17
+ Pagination: ({
18
+ currentPage,
19
+ totalPages,
20
+ onPageChange,
21
+ getPageHref,
22
+ ariaLabel,
23
+ }: any) => (
18
24
  <nav aria-label={ariaLabel} data-testid="pagination">
19
25
  <span>
20
26
  Page {currentPage} of {totalPages}
21
27
  </span>
22
- <button data-testid="page-next" onClick={() => onPageChange(2)}>
28
+ <a
29
+ data-testid="page-next"
30
+ href={getPageHref?.(2)}
31
+ onClick={() => onPageChange(2)}
32
+ >
23
33
  Next
24
- </button>
25
- <button data-testid="page-first" onClick={() => onPageChange(1)}>
34
+ </a>
35
+ <a
36
+ data-testid="page-first"
37
+ href={getPageHref?.(1)}
38
+ onClick={() => onPageChange(1)}
39
+ >
26
40
  First
27
- </button>
41
+ </a>
28
42
  </nav>
29
43
  ),
30
44
  }));
@@ -222,21 +236,38 @@ describe("BlogGrid", () => {
222
236
  });
223
237
 
224
238
  describe("Pagination", () => {
225
- it("navigates with page param on page change", () => {
239
+ it("builds page href with page param", () => {
226
240
  render(<BlogGrid {...defaultProps} />);
227
- fireEvent.click(screen.getByTestId("page-next"));
228
- expect(mockPush).toHaveBeenCalledWith("/blog?page=2", { scroll: false });
241
+ expect(screen.getByTestId("page-next")).toHaveAttribute(
242
+ "href",
243
+ "/blog?page=2"
244
+ );
245
+ });
246
+
247
+ it("removes page param when building href for page 1", () => {
248
+ mockSearchParams = new URLSearchParams("page=2");
249
+ render(<BlogGrid {...defaultProps} currentPage={2} />);
250
+ expect(screen.getByTestId("page-first")).toHaveAttribute("href", "/blog");
251
+ });
252
+
253
+ it("keeps other params when building page href", () => {
254
+ mockSearchParams = new URLSearchParams("category=tech");
255
+ render(<BlogGrid {...defaultProps} />);
256
+ expect(screen.getByTestId("page-first")).toHaveAttribute(
257
+ "href",
258
+ "/blog?category=tech"
259
+ );
260
+ });
261
+
262
+ it("scrolls to top when currentPage changes", () => {
263
+ const { rerender } = render(<BlogGrid {...defaultProps} />);
264
+ expect(window.scrollTo).not.toHaveBeenCalled();
265
+ rerender(<BlogGrid {...defaultProps} currentPage={2} />);
229
266
  expect(window.scrollTo).toHaveBeenCalledWith({
230
267
  top: 0,
231
268
  behavior: "smooth",
232
269
  });
233
270
  });
234
-
235
- it("removes page param when navigating to page 1", () => {
236
- render(<BlogGrid {...defaultProps} currentPage={2} />);
237
- fireEvent.click(screen.getByTestId("page-first"));
238
- expect(mockPush).toHaveBeenCalledWith("/blog?", { scroll: false });
239
- });
240
271
  });
241
272
 
242
273
  describe("Cover image handling", () => {
@@ -99,24 +99,27 @@ describe("EmailInputBlock", () => {
99
99
  const input = screen.getByPlaceholderText("Enter your email here");
100
100
  fireEvent.change(input, { target: { value: "invalid" } });
101
101
  fireEvent.click(screen.getByText("Sign me up"));
102
- expect(
103
- screen.getByText("Please enter a valid email address.")
104
- ).toBeInTheDocument();
102
+ expect(screen.getByText("Invalid email address")).toBeInTheDocument();
103
+ expect(defaultProps.onSubmit).not.toHaveBeenCalled();
104
+ });
105
+
106
+ it("shows required error for empty email", () => {
107
+ render(<EmailInputBlock {...defaultProps} />);
108
+ fireEvent.click(screen.getByText("Sign me up"));
109
+ expect(screen.getByText("Email is required")).toBeInTheDocument();
105
110
  expect(defaultProps.onSubmit).not.toHaveBeenCalled();
106
111
  });
107
112
 
108
- it("clears error when user types after error", () => {
113
+ it("clears error when user types a valid email after error", () => {
109
114
  render(<EmailInputBlock {...defaultProps} />);
110
115
  const input = screen.getByPlaceholderText("Enter your email here");
111
116
  fireEvent.change(input, { target: { value: "invalid" } });
112
117
  fireEvent.click(screen.getByText("Sign me up"));
113
- expect(
114
- screen.getByText("Please enter a valid email address.")
115
- ).toBeInTheDocument();
118
+ expect(screen.getByText("Invalid email address")).toBeInTheDocument();
116
119
 
117
- fireEvent.change(input, { target: { value: "a" } });
120
+ fireEvent.change(input, { target: { value: "user@test.com" } });
118
121
  expect(
119
- screen.queryByText("Please enter a valid email address.")
122
+ screen.queryByText("Invalid email address")
120
123
  ).not.toBeInTheDocument();
121
124
  });
122
125
 
@@ -148,62 +151,50 @@ describe("EmailInputBlock", () => {
148
151
  expect(screen.queryByText("Thanks!")).not.toBeInTheDocument();
149
152
  });
150
153
 
151
- it("applies green background by default", () => {
154
+ it("applies green theme background by default", () => {
152
155
  const { container } = render(<EmailInputBlock {...defaultProps} />);
153
- const inner = container.querySelector(".bg-bg-fill-success");
154
- expect(inner).toBeInTheDocument();
155
- });
156
-
157
- it("applies navy background class", () => {
158
- const { container } = render(
159
- <EmailInputBlock {...defaultProps} backgroundColor="navy" />
160
- );
161
- expect(container.querySelector(".bg-bg-fill-inverse")).toBeInTheDocument();
156
+ expect(container.querySelector(".bg-bg-fill-brand")).toBeInTheDocument();
162
157
  });
163
158
 
164
- it("applies blue background class", () => {
159
+ it("applies blue theme background class", () => {
165
160
  const { container } = render(
166
- <EmailInputBlock {...defaultProps} backgroundColor="blue" />
161
+ <EmailInputBlock {...defaultProps} themeColor="blue" />
167
162
  );
168
- expect(container.querySelector(".bg-bg-fill-brand")).toBeInTheDocument();
163
+ expect(
164
+ container.querySelector(".bg-bg-fill-brand-supporting")
165
+ ).toBeInTheDocument();
169
166
  });
170
167
 
171
- it("applies yellow background class", () => {
168
+ it("applies purple theme background class", () => {
172
169
  const { container } = render(
173
- <EmailInputBlock {...defaultProps} backgroundColor="yellow" />
170
+ <EmailInputBlock {...defaultProps} themeColor="purple" />
174
171
  );
175
172
  expect(
176
- container.querySelector(".bg-bg-fill-brand-accent")
173
+ container.querySelector(".bg-bg-fill-brand-tertiary")
177
174
  ).toBeInTheDocument();
178
175
  });
179
176
 
180
- it("applies white background class", () => {
181
- const { container } = render(
182
- <EmailInputBlock {...defaultProps} backgroundColor="white" />
183
- );
184
- expect(container.querySelector(".bg-white")).toBeInTheDocument();
177
+ it("applies white outer background class by default", () => {
178
+ const { container } = render(<EmailInputBlock {...defaultProps} />);
179
+ expect(container.querySelector("section")).toHaveClass("bg-bg");
185
180
  });
186
181
 
187
- it("uses white text for green/blue/navy backgrounds", () => {
182
+ it("uses white text for dark themes", () => {
188
183
  render(
189
- <EmailInputBlock
190
- {...defaultProps}
191
- backgroundColor="navy"
192
- title="Navy title"
193
- />
184
+ <EmailInputBlock {...defaultProps} themeColor="blue" title="Blue title" />
194
185
  );
195
- expect(screen.getByText("Navy title").className).toContain("text-white");
186
+ expect(screen.getByText("Blue title").className).toContain("text-white");
196
187
  });
197
188
 
198
- it("uses dark text for white/yellow backgrounds", () => {
189
+ it("uses dark text for the white theme", () => {
199
190
  render(
200
191
  <EmailInputBlock
201
192
  {...defaultProps}
202
- backgroundColor="yellow"
203
- title="Yellow title"
193
+ themeColor="white"
194
+ title="White title"
204
195
  />
205
196
  );
206
- expect(screen.getByText("Yellow title").className).toContain("text-text");
197
+ expect(screen.getByText("White title").className).toContain("text-text");
207
198
  });
208
199
 
209
200
  it("uses custom ctaText", () => {
@@ -260,12 +260,13 @@ describe("FindKinetic", () => {
260
260
  expect(screen.queryAllByRole("list")).toHaveLength(0);
261
261
  });
262
262
 
263
- it("renders two column layout for mobile and three for desktop", () => {
263
+ it("renders two column layout for mobile, three for tablet and four for desktop", () => {
264
264
  const { container } = render(<FindKinetic list={mockList} />);
265
265
  const lists = container.querySelectorAll("ul");
266
- expect(lists).toHaveLength(2);
266
+ expect(lists).toHaveLength(3);
267
267
  expect(lists[0]).toHaveClass("grid-cols-2");
268
268
  expect(lists[1]).toHaveClass("md:grid-cols-3");
269
+ expect(lists[2]).toHaveClass("xl:grid-cols-4");
269
270
  });
270
271
 
271
272
  it("rearranges items in column-major order for 2-col layout", () => {
@@ -89,7 +89,6 @@ const defaultProps: ImagePromoBarProps = {
89
89
  cta: null,
90
90
  secondaryCta: null,
91
91
  ctaDisclaimer: null,
92
- videoLink: { link: "", image: "", videoPopup: false },
93
92
  maxWidth: true,
94
93
  color: "light",
95
94
  };
@@ -182,15 +181,15 @@ describe("ImagePromoBar", () => {
182
181
  const { container } = render(
183
182
  <ImagePromoBar {...defaultProps} mediaPosition={true} />
184
183
  );
185
- expect(container.innerHTML).toContain("lg:flex-row-reverse");
184
+ expect(container.innerHTML).toContain("xl:flex-row-reverse");
186
185
  });
187
186
 
188
187
  it("applies lg:flex-row when mediaPosition is false", () => {
189
188
  const { container } = render(
190
189
  <ImagePromoBar {...defaultProps} mediaPosition={false} />
191
190
  );
192
- expect(container.innerHTML).toContain("lg:flex-row");
193
- expect(container.innerHTML).not.toContain("lg:flex-row-reverse");
191
+ expect(container.innerHTML).toContain("xl:flex-row");
192
+ expect(container.innerHTML).not.toContain("xl:flex-row-reverse");
194
193
  });
195
194
  });
196
195
 
@@ -276,22 +275,68 @@ describe("ImagePromoBar", () => {
276
275
  });
277
276
 
278
277
  describe("Image", () => {
279
- it("renders NextImage when image is provided", () => {
278
+ it("renders the plain image when there is no videoLink content", () => {
280
279
  render(
281
280
  <ImagePromoBar {...defaultProps} image="https://img.test/hero.jpg" />
282
281
  );
283
- const img = screen.getByTestId("next-image");
284
- expect(img).toHaveAttribute("src", "https://img.test/hero.jpg");
282
+ expect(screen.getByTestId("next-image")).toHaveAttribute(
283
+ "src",
284
+ "https://img.test/hero.jpg"
285
+ );
285
286
  });
286
287
 
287
- it("does not render image section when image is empty", () => {
288
+ it("does not render the image section when image is empty", () => {
288
289
  render(<ImagePromoBar {...defaultProps} image="" />);
289
290
  expect(screen.queryByTestId("next-image")).not.toBeInTheDocument();
290
291
  });
292
+
293
+ it("does not render the plain image when videoLink has a valid href", () => {
294
+ render(
295
+ <ImagePromoBar
296
+ {...defaultProps}
297
+ image="https://img.test/hero.jpg"
298
+ videoLink={{ link: "https://youtube.com/watch?v=123" }}
299
+ />
300
+ );
301
+ expect(screen.queryByTestId("next-image")).not.toBeInTheDocument();
302
+ });
303
+
304
+ it("does not render the plain image when videoLink has its own poster image, even without a valid href", () => {
305
+ render(
306
+ <ImagePromoBar
307
+ {...defaultProps}
308
+ image="https://img.test/hero.jpg"
309
+ videoLink={{ image: "thumb.jpg" }}
310
+ />
311
+ );
312
+ expect(screen.queryByTestId("next-image")).not.toBeInTheDocument();
313
+ });
314
+
315
+ it("renders the plain image when no videoLink is attached at all", () => {
316
+ render(
317
+ <ImagePromoBar
318
+ {...defaultProps}
319
+ image="https://img.test/hero.jpg"
320
+ videoLink={undefined}
321
+ />
322
+ );
323
+ expect(screen.getByTestId("next-image")).toBeInTheDocument();
324
+ });
325
+
326
+ it("suppresses the plain image when an empty videoLink object is attached (VideoLink owns its own fallback from there)", () => {
327
+ render(
328
+ <ImagePromoBar
329
+ {...defaultProps}
330
+ image="https://img.test/hero.jpg"
331
+ videoLink={{}}
332
+ />
333
+ );
334
+ expect(screen.queryByTestId("next-image")).not.toBeInTheDocument();
335
+ });
291
336
  });
292
337
 
293
338
  describe("Video", () => {
294
- it("renders VideoLink when videoLink is provided", () => {
339
+ it("always renders VideoLink, passing videoLink through", () => {
295
340
  render(
296
341
  <ImagePromoBar
297
342
  {...defaultProps}
@@ -317,7 +362,7 @@ describe("ImagePromoBar", () => {
317
362
  );
318
363
  });
319
364
 
320
- it("renders VideoLink even when videoLink has no link (VideoLink owns the gating)", () => {
365
+ it("still renders VideoLink when videoLink has no valid href (VideoLink owns its own poster fallback)", () => {
321
366
  render(<ImagePromoBar {...defaultProps} videoLink={{}} />);
322
367
  expect(screen.getByTestId("video-link")).toBeInTheDocument();
323
368
  });
@@ -36,6 +36,8 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
36
36
  renderCheckPlans,
37
37
  enableAnimation = false,
38
38
  }) => {
39
+ const hasVideoLink = Boolean(videoLink);
40
+
39
41
  return (
40
42
  <div className="component-container shared-component-ipb pb-16 pt-16">
41
43
  <div
@@ -161,8 +163,8 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
161
163
  )}
162
164
  </div>
163
165
  <aside className="flex w-full flex-1 shrink-0 items-center justify-center lg:w-auto">
164
- {/* Media Section */}
165
- {image && (
166
+ {/* Media Section: only when no Video Link is attached at all */}
167
+ {image && !hasVideoLink && (
166
168
  <div
167
169
  className={cx(
168
170
  !useOriginalImageAspectRatio
@@ -196,6 +198,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
196
198
  imageWidth={imageWidth}
197
199
  imageHeight={imageHeight}
198
200
  useOriginalImageAspectRatio={useOriginalImageAspectRatio}
201
+ isAboveTheFold={isAboveTheFold}
199
202
  />
200
203
  </aside>
201
204
  </div>
@@ -22,7 +22,7 @@ export type ImagePromoBarProps = {
22
22
  cta: any;
23
23
  secondaryCta: any;
24
24
  ctaDisclaimer: React.ReactNode;
25
- videoLink: VideoLinkProps;
25
+ videoLink?: VideoLinkProps;
26
26
  maxWidth?: boolean;
27
27
  color: "light" | "dark";
28
28
  enableAnimation?: boolean;