@windstream/react-shared-components 0.2.53 → 0.2.55

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,86 +1,86 @@
1
- import "@testing-library/jest-dom";
2
-
3
- import React from "react";
4
- import { AwardMarquee } from "./index";
5
- import { AwardMarqueeItem } from "./types";
6
-
7
- import { render, screen } from "@testing-library/react";
8
-
9
- const buildItems = (count: number): AwardMarqueeItem[] =>
10
- Array.from({ length: count }, (_, index) => ({
11
- iconUrl: `https://example.com/icon-${index}.png`,
12
- iconAlt: `Award ${index}`,
13
- title: `Award ${index}`,
14
- description: `Description ${index}`,
15
- }));
16
-
17
- describe("AwardMarquee", () => {
18
- it("renders award items synchronously (SSR, no hydration gate)", () => {
19
- render(<AwardMarquee items={buildItems(2)} />);
20
- expect(screen.getByText("Award 0")).toBeInTheDocument();
21
- expect(screen.getByText("Award 1")).toBeInTheDocument();
22
- });
23
-
24
- it("renders the icon with its alt text", () => {
25
- render(<AwardMarquee items={buildItems(1)} />);
26
- const icon = screen.getByAltText("Award 0") as HTMLImageElement;
27
- expect(icon).toBeInTheDocument();
28
- expect(icon.src).toContain("icon-0.png");
29
- });
30
-
31
- it("renders a static (non-animated) list when scroll is disabled", () => {
32
- const { container } = render(
33
- <AwardMarquee items={buildItems(5)} scroll={false} />
34
- );
35
- expect(container.querySelector(".animate-award-marquee")).toBeNull();
36
- });
37
-
38
- it("animates at every breakpoint when scroll is enabled with more than 3 items", () => {
39
- const { container } = render(
40
- <AwardMarquee items={buildItems(5)} scroll={true} />
41
- );
42
- expect(container.querySelector(".animate-award-marquee")).not.toBeNull();
43
- // No breakpoint split -> no mobile-only wrapper.
44
- expect(container.querySelector(".sm\\:hidden")).toBeNull();
45
- });
46
-
47
- it("renders both mobile (animated) and desktop (static) variants when scroll is enabled with 3 or fewer items", () => {
48
- const { container } = render(
49
- <AwardMarquee items={buildItems(2)} scroll={true} />
50
- );
51
- // Mobile variant animates and is hidden from >= sm.
52
- expect(container.querySelector(".sm\\:hidden")).not.toBeNull();
53
- expect(container.querySelector(".animate-award-marquee")).not.toBeNull();
54
- // Desktop variant is a static list shown only at >= sm.
55
- expect(container.querySelector(".hidden.sm\\:block")).not.toBeNull();
56
- // 2 items duplicated for the animated loop (4) + 2 in the static list.
57
- expect(screen.getAllByText("Award 0")).toHaveLength(3);
58
- });
59
-
60
- it("applies the anchorId to the wrapper in every render branch", () => {
61
- const staticView = render(
62
- <AwardMarquee
63
- items={buildItems(2)}
64
- scroll={false}
65
- anchorId="static-anchor"
66
- />
67
- );
68
- expect(staticView.container.querySelector("#static-anchor")).not.toBeNull();
69
-
70
- const animatedView = render(
71
- <AwardMarquee
72
- items={buildItems(5)}
73
- scroll={true}
74
- anchorId="animated-anchor"
75
- />
76
- );
77
- expect(
78
- animatedView.container.querySelector("#animated-anchor")
79
- ).not.toBeNull();
80
-
81
- const splitView = render(
82
- <AwardMarquee items={buildItems(2)} scroll={true} anchorId="split-anchor" />
83
- );
84
- expect(splitView.container.querySelector("#split-anchor")).not.toBeNull();
85
- });
86
- });
1
+ import "@testing-library/jest-dom";
2
+
3
+ import React from "react";
4
+ import { AwardMarquee } from "./index";
5
+ import { AwardMarqueeItem } from "./types";
6
+
7
+ import { render, screen } from "@testing-library/react";
8
+
9
+ const buildItems = (count: number): AwardMarqueeItem[] =>
10
+ Array.from({ length: count }, (_, index) => ({
11
+ iconUrl: `https://example.com/icon-${index}.png`,
12
+ iconAlt: `Award ${index}`,
13
+ title: `Award ${index}`,
14
+ description: `Description ${index}`,
15
+ }));
16
+
17
+ describe("AwardMarquee", () => {
18
+ it("renders award items synchronously (SSR, no hydration gate)", () => {
19
+ render(<AwardMarquee items={buildItems(2)} />);
20
+ expect(screen.getByText("Award 0")).toBeInTheDocument();
21
+ expect(screen.getByText("Award 1")).toBeInTheDocument();
22
+ });
23
+
24
+ it("renders the icon with its alt text", () => {
25
+ render(<AwardMarquee items={buildItems(1)} />);
26
+ const icon = screen.getByAltText("Award 0") as HTMLImageElement;
27
+ expect(icon).toBeInTheDocument();
28
+ expect(icon.src).toContain("icon-0.png");
29
+ });
30
+
31
+ it("renders a static (non-animated) list when scroll is disabled", () => {
32
+ const { container } = render(
33
+ <AwardMarquee items={buildItems(5)} scroll={false} />
34
+ );
35
+ expect(container.querySelector(".animate-award-marquee")).toBeNull();
36
+ });
37
+
38
+ it("animates at every breakpoint when scroll is enabled with more than 3 items", () => {
39
+ const { container } = render(
40
+ <AwardMarquee items={buildItems(5)} scroll={true} />
41
+ );
42
+ expect(container.querySelector(".animate-award-marquee")).not.toBeNull();
43
+ // No breakpoint split -> no mobile-only wrapper.
44
+ expect(container.querySelector(".sm\\:hidden")).toBeNull();
45
+ });
46
+
47
+ it("renders both mobile (animated) and desktop (static) variants when scroll is enabled with 3 or fewer items", () => {
48
+ const { container } = render(
49
+ <AwardMarquee items={buildItems(2)} scroll={true} />
50
+ );
51
+ // Mobile variant animates and is hidden from >= sm.
52
+ expect(container.querySelector(".sm\\:hidden")).not.toBeNull();
53
+ expect(container.querySelector(".animate-award-marquee")).not.toBeNull();
54
+ // Desktop variant is a static list shown only at >= sm.
55
+ expect(container.querySelector(".hidden.sm\\:block")).not.toBeNull();
56
+ // 2 items duplicated for the animated loop (4) + 2 in the static list.
57
+ expect(screen.getAllByText("Award 0")).toHaveLength(3);
58
+ });
59
+
60
+ it("applies the anchorId to the wrapper in every render branch", () => {
61
+ const staticView = render(
62
+ <AwardMarquee
63
+ items={buildItems(2)}
64
+ scroll={false}
65
+ anchorId="static-anchor"
66
+ />
67
+ );
68
+ expect(staticView.container.querySelector("#static-anchor")).not.toBeNull();
69
+
70
+ const animatedView = render(
71
+ <AwardMarquee
72
+ items={buildItems(5)}
73
+ scroll={true}
74
+ anchorId="animated-anchor"
75
+ />
76
+ );
77
+ expect(
78
+ animatedView.container.querySelector("#animated-anchor")
79
+ ).not.toBeNull();
80
+
81
+ const splitView = render(
82
+ <AwardMarquee items={buildItems(2)} scroll={true} anchorId="split-anchor" />
83
+ );
84
+ expect(splitView.container.querySelector("#split-anchor")).not.toBeNull();
85
+ });
86
+ });
@@ -1,110 +1,110 @@
1
- import React from "react";
2
- import {
3
- AwardMarqueeBackground,
4
- AwardMarqueeItem,
5
- AwardMarqueeProps,
6
- AwardMarqueeSpeed,
7
- } from "./types";
8
-
9
- import { cx } from "@shared/utils";
10
-
11
- const DURATION_IN_SECONDS: Record<AwardMarqueeSpeed, number> = {
12
- slow: 30,
13
- medium: 20,
14
- fast: 10,
15
- };
16
-
17
- const BACKGROUND_CLASS: Record<AwardMarqueeBackground, string> = {
18
- cream: "bg-[#FFFEEF]",
19
- gray90: "bg-[#FBFBFC]",
20
- white: "bg-white",
21
- };
22
-
23
- const renderAwardItem = (
24
- { iconUrl, iconAlt, title, description }: AwardMarqueeItem,
25
- key: React.Key
26
- ) => (
27
- <div
28
- key={key}
29
- className="ml-4 flex w-auto flex-shrink-0 items-center whitespace-nowrap"
30
- >
31
- <img src={iconUrl} alt={iconAlt ?? ""} width={200} />
32
- <div className="w-[250px] text-center">
33
- <p className="flex justify-center text-center text-body3 font-bold">
34
- {title}
35
- </p>
36
- <p className="flex flex-wrap whitespace-normal text-center text-footnote">
37
- {description}
38
- </p>
39
- </div>
40
- </div>
41
- );
42
-
43
- export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
44
- anchorId = "Award-Marquee",
45
- items = [],
46
- speed = "medium",
47
- scroll = false,
48
- backgroundColor = "white",
49
- className,
50
- }) => {
51
- const scrollAvailable = items.length > 3;
52
- const shouldDuplicate = items.length < 8;
53
- const duration = DURATION_IN_SECONDS[speed];
54
-
55
- const animatedMarquee = () => {
56
- const marqueeItems = shouldDuplicate ? [...items, ...items] : items;
57
-
58
- return (
59
- <div
60
- className={cx(
61
- "flex items-center overflow-x-hidden",
62
- BACKGROUND_CLASS[backgroundColor]
63
- )}
64
- >
65
- <div
66
- className="flex animate-award-marquee gap-4 px-2"
67
- style={
68
- {
69
- "--award-marquee-duration": `${duration}s`,
70
- } as React.CSSProperties
71
- }
72
- >
73
- {marqueeItems.map((item, index) => renderAwardItem(item, index))}
74
- </div>
75
- </div>
76
- );
77
- };
78
-
79
- const staticMarquee = (
80
- <div className="flex flex-nowrap items-center justify-around overflow-clip">
81
- {items.map((item, index) => renderAwardItem(item, index))}
82
- </div>
83
- );
84
-
85
- // No scroll: always static at every breakpoint.
86
- if (!scroll)
87
- return (
88
- <div id={anchorId} className={className}>
89
- {staticMarquee}
90
- </div>
91
- );
92
-
93
- // Scroll with >3 items: animate at every breakpoint.
94
- if (scrollAvailable)
95
- return (
96
- <div id={anchorId} className={className}>
97
- {animatedMarquee()}
98
- </div>
99
- );
100
-
101
- // Scroll with <=3 items: animate on mobile, static on >= sm (CSS-driven).
102
- return (
103
- <div id={anchorId} className={className}>
104
- <div className="sm:hidden">{animatedMarquee()}</div>
105
- <div className="hidden sm:block">{staticMarquee}</div>
106
- </div>
107
- );
108
- };
109
-
110
- export default AwardMarquee;
1
+ import React from "react";
2
+ import {
3
+ AwardMarqueeBackground,
4
+ AwardMarqueeItem,
5
+ AwardMarqueeProps,
6
+ AwardMarqueeSpeed,
7
+ } from "./types";
8
+
9
+ import { cx } from "@shared/utils";
10
+
11
+ const DURATION_IN_SECONDS: Record<AwardMarqueeSpeed, number> = {
12
+ slow: 30,
13
+ medium: 20,
14
+ fast: 10,
15
+ };
16
+
17
+ const BACKGROUND_CLASS: Record<AwardMarqueeBackground, string> = {
18
+ cream: "bg-[#FFFEEF]",
19
+ gray90: "bg-[#FBFBFC]",
20
+ white: "bg-white",
21
+ };
22
+
23
+ const renderAwardItem = (
24
+ { iconUrl, iconAlt, title, description }: AwardMarqueeItem,
25
+ key: React.Key
26
+ ) => (
27
+ <div
28
+ key={key}
29
+ className="ml-4 flex w-auto flex-shrink-0 items-center whitespace-nowrap"
30
+ >
31
+ <img src={iconUrl} alt={iconAlt ?? ""} width={200} />
32
+ <div className="w-[250px] text-center">
33
+ <p className="flex justify-center text-center text-body3 font-bold">
34
+ {title}
35
+ </p>
36
+ <p className="flex flex-wrap whitespace-normal text-center text-footnote">
37
+ {description}
38
+ </p>
39
+ </div>
40
+ </div>
41
+ );
42
+
43
+ export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
44
+ anchorId = "Award-Marquee",
45
+ items = [],
46
+ speed = "medium",
47
+ scroll = false,
48
+ backgroundColor = "white",
49
+ className,
50
+ }) => {
51
+ const scrollAvailable = items.length > 3;
52
+ const shouldDuplicate = items.length < 8;
53
+ const duration = DURATION_IN_SECONDS[speed];
54
+
55
+ const animatedMarquee = () => {
56
+ const marqueeItems = shouldDuplicate ? [...items, ...items] : items;
57
+
58
+ return (
59
+ <div
60
+ className={cx(
61
+ "flex items-center overflow-x-hidden",
62
+ BACKGROUND_CLASS[backgroundColor]
63
+ )}
64
+ >
65
+ <div
66
+ className="flex animate-award-marquee gap-4 px-2"
67
+ style={
68
+ {
69
+ "--award-marquee-duration": `${duration}s`,
70
+ } as React.CSSProperties
71
+ }
72
+ >
73
+ {marqueeItems.map((item, index) => renderAwardItem(item, index))}
74
+ </div>
75
+ </div>
76
+ );
77
+ };
78
+
79
+ const staticMarquee = (
80
+ <div className="flex flex-nowrap items-center justify-around overflow-clip">
81
+ {items.map((item, index) => renderAwardItem(item, index))}
82
+ </div>
83
+ );
84
+
85
+ // No scroll: always static at every breakpoint.
86
+ if (!scroll)
87
+ return (
88
+ <div id={anchorId} className={className}>
89
+ {staticMarquee}
90
+ </div>
91
+ );
92
+
93
+ // Scroll with >3 items: animate at every breakpoint.
94
+ if (scrollAvailable)
95
+ return (
96
+ <div id={anchorId} className={className}>
97
+ {animatedMarquee()}
98
+ </div>
99
+ );
100
+
101
+ // Scroll with <=3 items: animate on mobile, static on >= sm (CSS-driven).
102
+ return (
103
+ <div id={anchorId} className={className}>
104
+ <div className="sm:hidden">{animatedMarquee()}</div>
105
+ <div className="hidden sm:block">{staticMarquee}</div>
106
+ </div>
107
+ );
108
+ };
109
+
110
+ export default AwardMarquee;
@@ -1,19 +1,19 @@
1
- export type AwardMarqueeSpeed = "fast" | "medium" | "slow";
2
-
3
- export type AwardMarqueeBackground = "cream" | "gray90" | "white";
4
-
5
- export type AwardMarqueeItem = {
6
- iconUrl: string;
7
- iconAlt?: string;
8
- title?: string;
9
- description?: string;
10
- };
11
-
12
- export type AwardMarqueeProps = {
13
- anchorId?: string;
14
- items: AwardMarqueeItem[];
15
- speed?: AwardMarqueeSpeed;
16
- scroll?: boolean;
17
- backgroundColor?: AwardMarqueeBackground;
18
- className?: string;
19
- };
1
+ export type AwardMarqueeSpeed = "fast" | "medium" | "slow";
2
+
3
+ export type AwardMarqueeBackground = "cream" | "gray90" | "white";
4
+
5
+ export type AwardMarqueeItem = {
6
+ iconUrl: string;
7
+ iconAlt?: string;
8
+ title?: string;
9
+ description?: string;
10
+ };
11
+
12
+ export type AwardMarqueeProps = {
13
+ anchorId?: string;
14
+ items: AwardMarqueeItem[];
15
+ speed?: AwardMarqueeSpeed;
16
+ scroll?: boolean;
17
+ backgroundColor?: AwardMarqueeBackground;
18
+ className?: string;
19
+ };
@@ -0,0 +1,97 @@
1
+ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import * as Yup from "yup";
3
+ import { FiberForm } from ".";
4
+ import type { FiberFormValues } from "./types";
5
+
6
+ jest.mock("@vimeo/player", () => jest.fn(), { virtual: true });
7
+
8
+ const initialValues: FiberFormValues = {
9
+ firstName: "",
10
+ lastName: "",
11
+ address: "",
12
+ phone: "",
13
+ email: "",
14
+ unit: "",
15
+ zip: "",
16
+ isManualAddress: false,
17
+ isMarketingCom: false,
18
+ addressToSend: {},
19
+ notes: "",
20
+ referralCode: "",
21
+ hasUnits: false,
22
+ };
23
+
24
+ const AddressAutocomplete = () => null;
25
+
26
+ const deferred = () => {
27
+ let resolve!: () => void;
28
+ let reject!: (error: Error) => void;
29
+ const promise = new Promise<void>((resolvePromise, rejectPromise) => {
30
+ resolve = resolvePromise;
31
+ reject = rejectPromise;
32
+ });
33
+ return { promise, resolve, reject };
34
+ };
35
+
36
+ const renderForm = (onSubmit: (values: FiberFormValues) => Promise<void>) =>
37
+ render(
38
+ <FiberForm
39
+ AddressAutocomplete={AddressAutocomplete}
40
+ initialValues={initialValues}
41
+ inputList={{}}
42
+ formSchema={Yup.object({})}
43
+ onSubmit={onSubmit}
44
+ buttonText="Submit"
45
+ hideLegalCopy={true}
46
+ />
47
+ );
48
+
49
+ describe("FiberForm submit loading state", () => {
50
+ it("shows a spinner and disables submit until submission resolves", async () => {
51
+ const submission = deferred();
52
+ const onSubmit = jest.fn(() => submission.promise);
53
+ renderForm(onSubmit);
54
+ const button = screen.getByRole("button", { name: /submit/i });
55
+
56
+ expect(button).toHaveClass("w-[198px]", "max-w-full");
57
+
58
+ fireEvent.click(button);
59
+
60
+ await waitFor(() => expect(button).toBeDisabled());
61
+ expect(button).toHaveAttribute("aria-busy", "true");
62
+ expect(button).toHaveClass("opacity-75");
63
+ expect(screen.getByRole("status")).toBeInTheDocument();
64
+
65
+ fireEvent.click(button);
66
+ expect(onSubmit).toHaveBeenCalledTimes(1);
67
+
68
+ await act(async () => submission.resolve());
69
+
70
+ await waitFor(() => expect(button).toBeEnabled());
71
+ expect(button).toHaveAttribute("aria-busy", "false");
72
+ expect(button).not.toHaveClass("opacity-75");
73
+ expect(screen.queryByRole("status")).not.toBeInTheDocument();
74
+ });
75
+
76
+ it("returns submit to normal after a handled rejection", async () => {
77
+ const submission = deferred();
78
+ renderForm(async () => {
79
+ try {
80
+ await submission.promise;
81
+ } catch {
82
+ return;
83
+ }
84
+ });
85
+ const button = screen.getByRole("button", { name: /submit/i });
86
+
87
+ fireEvent.click(button);
88
+
89
+ await waitFor(() => expect(button).toBeDisabled());
90
+ expect(screen.getByRole("status")).toBeInTheDocument();
91
+
92
+ await act(async () => submission.reject(new Error("Request failed")));
93
+
94
+ await waitFor(() => expect(button).toBeEnabled());
95
+ expect(screen.queryByRole("status")).not.toBeInTheDocument();
96
+ });
97
+ });
@@ -5,6 +5,7 @@ import { Button } from "../../../components/button";
5
5
  import { Input } from "../../../components/input";
6
6
  import { MaterialIcon } from "../../../components/material-icon";
7
7
  import { Select, type SelectOption } from "../../../components/select";
8
+ import { Spinner } from "../../../components/spinner";
8
9
  import { Text } from "../../../components/text";
9
10
  import { Tooltip } from "../../../components/tooltip";
10
11
  import { cx } from "../../../utils";
@@ -160,7 +161,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
160
161
  setFieldValue,
161
162
  values,
162
163
  }) => (
163
- <Form className={cx("w-full", className)}>
164
+ <Form className={cx("fiber-form w-full", className)}>
164
165
  <div className="flex flex-col gap-5 sm:flex-row">
165
166
  {inputList.firstName && (
166
167
  <div className="flex-1">
@@ -259,7 +260,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
259
260
  >
260
261
  <button
261
262
  type="button"
262
- className="flex w-full items-center gap-2 px-4 py-3 text-left hover:bg-bg-surface-hover"
263
+ className="body2 flex w-full items-center gap-2 px-4 py-3 text-left hover:bg-bg-surface-hover"
263
264
  onClick={() =>
264
265
  selectCandidate(candidate, setFieldValue)
265
266
  }
@@ -487,7 +488,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
487
488
  </label>
488
489
  <Tooltip
489
490
  tooltipMsg="By opting in to receive text messages, you will receive automated text message related to service notifications including installations, service changes and updates, billing information, marketing offers from Kinetic including cart reminders. Consent is not a condition of purchase. Reply STOP at any time to unsubscribe. Message frequency varies. Msg & data rates may apply. To contact our Customer Support via email, chat, or phone, visit our Support page below."
490
- className="ff-NR mt-0"
491
+ className="fiber-form-tooltip ff-NR mt-0"
491
492
  >
492
493
  <MaterialIcon
493
494
  name="info"
@@ -531,16 +532,22 @@ export const FiberForm: React.FC<FiberFormProps> = ({
531
532
  ))}
532
533
  </ul>
533
534
  )}
534
- <div className="mt-8 flex items-center justify-center px-[60px] lg:mt-[64px]">
535
+ <div className="mt-8 flex items-center justify-center lg:mt-[64px]">
535
536
  <Button
536
537
  type="submit"
537
538
  disabled={isSubmitting}
538
539
  className={cx(
539
- "min-h-14 rounded-xl bg-bg-fill-brand px-8 py-4 font-bold text-white disabled:opacity-50",
540
+ "flex h-16 w-[198px] max-w-full cursor-pointer items-center justify-center overflow-hidden rounded-button border border-brand bg-bg-fill-brand px-[60px] py-4 text-center font-black leading-7 text-text-brand-on-bg-fill no-underline transition-colors duration-300 ease-in-out enabled:hover:border-[var(--color-bg-fill-brand-hover)] enabled:hover:bg-bg-fill-brand-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-brand disabled:cursor-not-allowed",
541
+ isSubmitting && "opacity-75",
540
542
  buttonClassName
541
543
  )}
542
544
  >
543
- {isSubmitting ? "Submitting..." : buttonText}
545
+ <span className="relative flex w-full items-center justify-center">
546
+ {isSubmitting && (
547
+ <Spinner size="small" className="!h-5 !w-5" />
548
+ )}
549
+ <span>{buttonText}</span>
550
+ </span>
544
551
  </Button>
545
552
  </div>
546
553
  </Form>
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { PrimaryHero } from "./index";
3
3
 
4
- import { render, screen } from "@testing-library/react";
4
+ import { fireEvent, render, screen } from "@testing-library/react";
5
5
 
6
6
  jest.mock("@shared/components/checklist", () => ({
7
7
  Checklist: ({ items }: any) => (
@@ -14,8 +14,14 @@ jest.mock("@shared/components/checklist", () => ({
14
14
  }));
15
15
 
16
16
  jest.mock("@shared/components/next-image", () => ({
17
- NextImage: ({ src, alt, className }: any) => (
18
- <img data-testid="next-image" src={src} alt={alt} className={className} />
17
+ NextImage: ({ src, alt, className, onClick }: any) => (
18
+ <img
19
+ data-testid="next-image"
20
+ src={src}
21
+ alt={alt}
22
+ className={className}
23
+ onClick={onClick}
24
+ />
19
25
  ),
20
26
  }));
21
27
 
@@ -347,6 +353,25 @@ describe("PrimaryHero", () => {
347
353
  expect(badges).toHaveLength(2);
348
354
  });
349
355
 
356
+ it("adds cursor class and triggers onImageClick for badge image", () => {
357
+ const onImageClick = jest.fn();
358
+
359
+ render(
360
+ <PrimaryHero
361
+ {...baseProps}
362
+ badgeImage="https://img.test/badge.png"
363
+ onImageClick={onImageClick}
364
+ />
365
+ );
366
+
367
+ const badges = screen.getAllByAltText("Badge");
368
+ expect(badges).toHaveLength(2);
369
+ badges.forEach((badge) => expect(badge).toHaveClass("cursor-pointer"));
370
+
371
+ fireEvent.click(badges[0]);
372
+ expect(onImageClick).toHaveBeenCalledTimes(1);
373
+ });
374
+
350
375
  it("does not render badge image when not provided", () => {
351
376
  render(<PrimaryHero {...baseProps} />);
352
377
  expect(screen.queryByAltText("Badge")).not.toBeInTheDocument();