@windstream/react-shared-components 0.1.93 → 0.1.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/contentful/index.esm.js +2 -2
  2. package/dist/contentful/index.esm.js.map +1 -1
  3. package/dist/contentful/index.js +3 -3
  4. package/dist/contentful/index.js.map +1 -1
  5. package/dist/core.d.ts +2 -2
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.esm.js +1 -1
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/index.js +3 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/styles.css +1 -1
  12. package/dist/utils/index.esm.js +1 -1
  13. package/dist/utils/index.js +1 -1
  14. package/package.json +14 -8
  15. package/src/components/accordion/index.test.tsx +270 -0
  16. package/src/components/alert-card/index.test.tsx +152 -0
  17. package/src/components/animation-wrapper/index.test.tsx +424 -0
  18. package/src/components/brand-button/index.test.tsx +292 -0
  19. package/src/components/button/index.test.tsx +91 -0
  20. package/src/components/call-button/index.test.tsx +260 -0
  21. package/src/components/checkbox/index.test.tsx +252 -0
  22. package/src/components/checklist/index.test.tsx +231 -0
  23. package/src/components/checklist/index.tsx +64 -29
  24. package/src/components/checklist/types.ts +7 -1
  25. package/src/components/collapse/index.test.tsx +277 -0
  26. package/src/components/collapse/index.tsx +1 -0
  27. package/src/components/divider/index.test.tsx +53 -0
  28. package/src/components/image/index.test.tsx +174 -0
  29. package/src/components/input/index.test.tsx +348 -0
  30. package/src/components/link/index.test.tsx +199 -0
  31. package/src/components/list/index.test.tsx +166 -0
  32. package/src/components/material-icon/index.test.tsx +130 -0
  33. package/src/components/modal/index.test.tsx +310 -0
  34. package/src/components/next-image/index.test.tsx +406 -0
  35. package/src/components/pagination/index.test.tsx +521 -0
  36. package/src/components/radio-button/index.test.tsx +151 -0
  37. package/src/components/see-more/index.test.tsx +96 -0
  38. package/src/components/select/index.test.tsx +256 -0
  39. package/src/components/select-plan-button/index.test.tsx +173 -0
  40. package/src/components/skeleton/index.test.tsx +74 -0
  41. package/src/components/spinner/index.test.tsx +76 -0
  42. package/src/components/text/index.test.tsx +65 -0
  43. package/src/components/tooltip/index.test.tsx +50 -0
  44. package/src/components/view-cart-button/index.test.tsx +57 -0
  45. package/src/contentful/blocks/accordion/index.test.tsx +218 -0
  46. package/src/contentful/blocks/accordion/index.tsx +3 -1
  47. package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -0
  48. package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -0
  49. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +5 -4
  50. package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -0
  51. package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -0
  52. package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -0
  53. package/src/contentful/blocks/button/index.test.tsx +339 -0
  54. package/src/contentful/blocks/callout/index.test.tsx +539 -0
  55. package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -0
  56. package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -0
  57. package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -0
  58. package/src/contentful/blocks/cards/index.test.tsx +39 -0
  59. package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -0
  60. package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -0
  61. package/src/contentful/blocks/cards/simple-card/index.tsx +1 -1
  62. package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -0
  63. package/src/contentful/blocks/carousel/helper.test.tsx +539 -0
  64. package/src/contentful/blocks/carousel/index.test.tsx +308 -0
  65. package/src/contentful/blocks/carousel/types.test.ts +16 -0
  66. package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -0
  67. package/src/contentful/blocks/cart-retention-banner/index.tsx +4 -4
  68. package/src/contentful/blocks/comparison-table/index.test.tsx +114 -0
  69. package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -0
  70. package/src/contentful/blocks/cta-callout/index.test.tsx +244 -0
  71. package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -0
  72. package/src/contentful/blocks/email-input-block/index.test.tsx +213 -0
  73. package/src/contentful/blocks/email-input-block/index.tsx +40 -35
  74. package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -0
  75. package/src/contentful/blocks/floating-banner/index.test.tsx +246 -0
  76. package/src/contentful/blocks/footer/index.test.tsx +302 -0
  77. package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -0
  78. package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -0
  79. package/src/contentful/blocks/image-promo-bar/index.tsx +248 -246
  80. package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -0
  81. package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -0
  82. package/src/contentful/blocks/modal/index.test.tsx +209 -0
  83. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -0
  84. package/src/contentful/blocks/navigation/index.test.tsx +924 -0
  85. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -0
  86. package/src/contentful/blocks/primary-hero/index.test.tsx +286 -0
  87. package/src/contentful/blocks/primary-hero/index.tsx +7 -4
  88. package/src/contentful/blocks/search-block/index.test.tsx +268 -0
  89. package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -0
  90. package/src/contentful/blocks/text/index.test.tsx +36 -0
  91. package/src/contentful/index.test.ts +45 -0
  92. package/src/global-mocks/contentful/to-document.ts +25 -0
  93. package/src/global-mocks/cookie.ts +48 -0
  94. package/src/global-mocks/cx.ts +37 -0
  95. package/src/global-mocks/index.ts +89 -0
  96. package/src/global-mocks/speed-card-bg.ts +27 -0
  97. package/src/global-mocks/utm.ts +49 -0
  98. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -0
  99. package/src/hooks/contentful/use-contentful-rich-text.tsx +1 -1
  100. package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -0
  101. package/src/hooks/use-body-scroll-lock.test.ts +134 -0
  102. package/src/hooks/use-carousel-swipe.test.ts +393 -0
  103. package/src/hooks/use-outside-click.test.ts +142 -0
  104. package/src/index.ts +1 -1
  105. package/src/next/index.test.ts +7 -0
  106. package/src/setupTests.ts +17 -11
  107. package/src/utils/contentful/to-document.test.ts +85 -0
  108. package/src/utils/cookie.test.ts +180 -0
  109. package/src/utils/cx.test.ts +90 -0
  110. package/src/utils/index.test.ts +115 -0
  111. package/src/utils/speed-card-bg.test.ts +46 -0
  112. package/src/utils/utm.test.ts +359 -0
@@ -0,0 +1,104 @@
1
+ import React from "react";
2
+ import { YoutubeEmbed } from "./youtube-embed";
3
+
4
+ import { render, screen } from "@testing-library/react";
5
+
6
+ jest.mock("@shared/utils", () => ({
7
+ cx: (...args: any[]) => args.filter(Boolean).join(" "),
8
+ }));
9
+
10
+ describe("YoutubeEmbed", () => {
11
+ it("renders iframe with extracted video id from watch URL", () => {
12
+ render(<YoutubeEmbed link="https://www.youtube.com/watch?v=dQw4w9WgXcQ" />);
13
+ const iframe = screen.getByTitle("Embedded youtube");
14
+ expect(iframe.getAttribute("src")).toBe(
15
+ "https://www.youtube.com/embed/dQw4w9WgXcQ"
16
+ );
17
+ });
18
+
19
+ it("extracts id from youtu.be short URL", () => {
20
+ render(<YoutubeEmbed link="https://youtu.be/dQw4w9WgXcQ" />);
21
+ expect(screen.getByTitle("Embedded youtube").getAttribute("src")).toContain(
22
+ "/embed/dQw4w9WgXcQ"
23
+ );
24
+ });
25
+
26
+ it("extracts id from embed URL", () => {
27
+ render(<YoutubeEmbed link="https://www.youtube.com/embed/dQw4w9WgXcQ" />);
28
+ expect(screen.getByTitle("Embedded youtube").getAttribute("src")).toContain(
29
+ "/embed/dQw4w9WgXcQ"
30
+ );
31
+ });
32
+
33
+ it("returns empty embedId for invalid URL", () => {
34
+ render(<YoutubeEmbed link="https://example.com/notavideo" />);
35
+ expect(screen.getByTitle("Embedded youtube").getAttribute("src")).toBe(
36
+ "https://www.youtube.com/embed/"
37
+ );
38
+ });
39
+
40
+ it("returns empty embedId when link is empty", () => {
41
+ render(<YoutubeEmbed link="" />);
42
+ expect(screen.getByTitle("Embedded youtube").getAttribute("src")).toBe(
43
+ "https://www.youtube.com/embed/"
44
+ );
45
+ });
46
+
47
+ it("returns empty embedId when link is undefined", () => {
48
+ render(<YoutubeEmbed link={undefined} />);
49
+ expect(screen.getByTitle("Embedded youtube").getAttribute("src")).toBe(
50
+ "https://www.youtube.com/embed/"
51
+ );
52
+ });
53
+
54
+ it("appends autoplay param when autoplay is true", () => {
55
+ render(
56
+ <YoutubeEmbed
57
+ link="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
58
+ autoplay={true}
59
+ />
60
+ );
61
+ expect(screen.getByTitle("Embedded youtube").getAttribute("src")).toBe(
62
+ "https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1"
63
+ );
64
+ });
65
+
66
+ it("does not append autoplay param when autoplay is false", () => {
67
+ render(
68
+ <YoutubeEmbed
69
+ link="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
70
+ autoplay={false}
71
+ />
72
+ );
73
+ expect(
74
+ screen.getByTitle("Embedded youtube").getAttribute("src")
75
+ ).not.toContain("autoplay");
76
+ });
77
+
78
+ it("applies containerClassName", () => {
79
+ const { container } = render(
80
+ <YoutubeEmbed
81
+ link="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
82
+ containerClassName="extra"
83
+ />
84
+ );
85
+ expect(container.firstElementChild?.className).toContain("extra");
86
+ });
87
+
88
+ it("has correct iframe attributes", () => {
89
+ render(<YoutubeEmbed link="https://www.youtube.com/watch?v=dQw4w9WgXcQ" />);
90
+ const iframe = screen.getByTitle("Embedded youtube");
91
+ expect(iframe).toHaveAttribute(
92
+ "allow",
93
+ "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
94
+ );
95
+ expect(iframe).toHaveAttribute("allowFullScreen");
96
+ });
97
+
98
+ it("returns empty embedId when match length is not 11", () => {
99
+ render(<YoutubeEmbed link="https://www.youtube.com/watch?v=short" />);
100
+ expect(screen.getByTitle("Embedded youtube").getAttribute("src")).toBe(
101
+ "https://www.youtube.com/embed/"
102
+ );
103
+ });
104
+ });
@@ -0,0 +1,209 @@
1
+ import React from "react";
2
+ import { Modal } from "./index";
3
+
4
+ import { fireEvent, render, screen } from "@testing-library/react";
5
+
6
+ jest.mock("framer-motion", () => ({
7
+ motion: {
8
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
9
+ div: ({ children, className, ...rest }: any) => (
10
+ <div className={className}>{children}</div>
11
+ ),
12
+ },
13
+ }));
14
+
15
+ jest.mock("@radix-ui/react-dialog", () => ({
16
+ Root: ({ open, onOpenChange, children }: any) => (
17
+ <div
18
+ data-testid="dialog-root"
19
+ data-open={open}
20
+ onClick={() => open && onOpenChange?.()}
21
+ >
22
+ {children}
23
+ </div>
24
+ ),
25
+ Portal: ({ children }: any) => (
26
+ <div data-testid="dialog-portal">{children}</div>
27
+ ),
28
+ Overlay: () => <div data-testid="dialog-overlay" />,
29
+ Content: ({ children, className, style, onOpenAutoFocus }: any) => {
30
+ // Call onOpenAutoFocus to cover that branch
31
+ if (onOpenAutoFocus) {
32
+ const mockEvent = { preventDefault: jest.fn() };
33
+ onOpenAutoFocus(mockEvent);
34
+ }
35
+ return (
36
+ <div data-testid="dialog-content" className={className} style={style}>
37
+ {children}
38
+ </div>
39
+ );
40
+ },
41
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
42
+ Close: ({ children, asChild }: any) => (
43
+ <div data-testid="dialog-close">{children}</div>
44
+ ),
45
+ }));
46
+
47
+ jest.mock("@shared/components/material-icon", () => ({
48
+ MaterialIcon: ({ name }: any) => (
49
+ <span data-testid="material-icon">{name}</span>
50
+ ),
51
+ }));
52
+
53
+ jest.mock("@shared/components/text", () => ({
54
+ Text: ({ as: Tag = "span", children, className }: any) => (
55
+ <Tag className={className}>{children}</Tag>
56
+ ),
57
+ }));
58
+
59
+ jest.mock("@shared/contentful/blocks/button", () => ({
60
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
61
+ Button: ({ children, buttonClassName, showButtonAs }: any) => (
62
+ <button data-testid="close-button" className={buttonClassName}>
63
+ {children}
64
+ </button>
65
+ ),
66
+ }));
67
+
68
+ jest.mock("@shared/utils", () => ({
69
+ cx: (...args: any[]) => args.filter(Boolean).join(" "),
70
+ }));
71
+
72
+ describe("Modal", () => {
73
+ const defaultProps = { isOpen: true, onRequestClose: jest.fn() };
74
+
75
+ beforeEach(() => jest.clearAllMocks());
76
+
77
+ describe("Rendering", () => {
78
+ it("renders dialog root with open state", () => {
79
+ render(<Modal {...defaultProps} />);
80
+ expect(screen.getByTestId("dialog-root")).toHaveAttribute(
81
+ "data-open",
82
+ "true"
83
+ );
84
+ });
85
+
86
+ it("renders close button with MaterialIcon", () => {
87
+ render(<Modal {...defaultProps} />);
88
+ expect(screen.getByTestId("close-button")).toBeInTheDocument();
89
+ expect(screen.getByText("close")).toBeInTheDocument();
90
+ });
91
+
92
+ it("renders title as h2 when provided", () => {
93
+ render(<Modal {...defaultProps} title="My Title" />);
94
+ expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
95
+ "My Title"
96
+ );
97
+ });
98
+
99
+ it("does not render title when not provided", () => {
100
+ render(<Modal {...defaultProps} />);
101
+ expect(screen.queryByRole("heading")).not.toBeInTheDocument();
102
+ });
103
+
104
+ it("renders description when provided", () => {
105
+ render(<Modal {...defaultProps} description="Some desc" />);
106
+ expect(screen.getByText("Some desc")).toBeInTheDocument();
107
+ });
108
+
109
+ it("does not render description when not provided", () => {
110
+ const { container } = render(<Modal {...defaultProps} />);
111
+ expect(container.querySelector(".mb-8")).not.toBeInTheDocument();
112
+ });
113
+
114
+ it("renders content", () => {
115
+ render(
116
+ <Modal
117
+ {...defaultProps}
118
+ content={<div data-testid="content">Content</div>}
119
+ />
120
+ );
121
+ expect(screen.getByTestId("content")).toBeInTheDocument();
122
+ });
123
+
124
+ it("renders children", () => {
125
+ render(
126
+ <Modal {...defaultProps}>
127
+ <span data-testid="child">Child</span>
128
+ </Modal>
129
+ );
130
+ expect(screen.getByTestId("child")).toBeInTheDocument();
131
+ });
132
+
133
+ it("does not render children placeholder when not provided", () => {
134
+ const { container } = render(<Modal {...defaultProps} />);
135
+ expect(container.innerHTML).not.toContain("null");
136
+ });
137
+ });
138
+
139
+ describe("Size", () => {
140
+ it("applies lg maxWidth by default", () => {
141
+ render(<Modal {...defaultProps} />);
142
+ expect(screen.getByTestId("dialog-content").style.maxWidth).toBe(
143
+ "1024px"
144
+ );
145
+ });
146
+
147
+ it("applies sm maxWidth", () => {
148
+ render(<Modal {...defaultProps} size="sm" />);
149
+ expect(screen.getByTestId("dialog-content").style.maxWidth).toBe("640px");
150
+ });
151
+
152
+ it("applies xl maxWidth", () => {
153
+ render(<Modal {...defaultProps} size="xl" />);
154
+ expect(screen.getByTestId("dialog-content").style.maxWidth).toBe(
155
+ "1280px"
156
+ );
157
+ });
158
+ });
159
+
160
+ describe("Props", () => {
161
+ it("applies bodyClassName to content", () => {
162
+ render(<Modal {...defaultProps} bodyClassName="custom-body" />);
163
+ expect(screen.getByTestId("dialog-content").className).toContain(
164
+ "custom-body"
165
+ );
166
+ });
167
+
168
+ it("applies type and index as data attributes", () => {
169
+ render(<Modal {...defaultProps} type="tabmodal" index={2} />);
170
+ const section = screen
171
+ .getByTestId("dialog-content")
172
+ .querySelector("[data-section-type]");
173
+ expect(section).toHaveAttribute("data-section-type", "tabmodal");
174
+ expect(section).toHaveAttribute("data-section-index", "2");
175
+ });
176
+
177
+ it("defaults type to modal and index to 0", () => {
178
+ render(<Modal {...defaultProps} />);
179
+ const section = screen
180
+ .getByTestId("dialog-content")
181
+ .querySelector("[data-section-type]");
182
+ expect(section).toHaveAttribute("data-section-type", "modal");
183
+ expect(section).toHaveAttribute("data-section-index", "0");
184
+ });
185
+ });
186
+
187
+ describe("Close behavior", () => {
188
+ it("calls onRequestClose when dialog onOpenChange triggers while open", () => {
189
+ render(<Modal {...defaultProps} />);
190
+ fireEvent.click(screen.getByTestId("dialog-root"));
191
+ expect(defaultProps.onRequestClose).toHaveBeenCalled();
192
+ });
193
+
194
+ it("does not call onRequestClose when isOpen is false", () => {
195
+ render(
196
+ <Modal isOpen={false} onRequestClose={defaultProps.onRequestClose} />
197
+ );
198
+ fireEvent.click(screen.getByTestId("dialog-root"));
199
+ expect(defaultProps.onRequestClose).not.toHaveBeenCalled();
200
+ });
201
+
202
+ it("handles missing onRequestClose gracefully", () => {
203
+ expect(() => {
204
+ render(<Modal isOpen={true} />);
205
+ fireEvent.click(screen.getByTestId("dialog-root"));
206
+ }).not.toThrow();
207
+ });
208
+ });
209
+ });
@@ -0,0 +1,208 @@
1
+ import React from "react";
2
+ import { DesktopLinkGroups } from "./index";
3
+
4
+ import { act, fireEvent, render, screen } from "@testing-library/react";
5
+
6
+ jest.mock("@shared/components/material-icon", () => ({
7
+ MaterialIcon: ({ name, className }: any) => (
8
+ <span data-testid={`icon-${name}`} className={className}>
9
+ {name}
10
+ </span>
11
+ ),
12
+ }));
13
+
14
+ jest.mock("@shared/components/text", () => ({
15
+ Text: ({ as: Tag = "span", children, className }: any) => (
16
+ <Tag className={className}>{children}</Tag>
17
+ ),
18
+ }));
19
+
20
+ jest.mock("@shared/contentful/blocks/button", () => ({
21
+ Button: ({
22
+ children,
23
+ buttonClassName,
24
+ onClick,
25
+ linkClassName,
26
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
27
+ showButtonAs,
28
+ ...rest
29
+ }: any) => (
30
+ <button
31
+ data-testid="button"
32
+ className={buttonClassName || linkClassName}
33
+ onClick={onClick}
34
+ aria-expanded={rest["aria-expanded"]}
35
+ >
36
+ {children || rest.buttonLabel}
37
+ </button>
38
+ ),
39
+ }));
40
+
41
+ let outsideClickCallback: (() => void) | null = null;
42
+ jest.mock("@shared/hooks/use-outside-click", () => ({
43
+ useOutsideClick: (_ref: any, cb: () => void) => {
44
+ outsideClickCallback = cb;
45
+ },
46
+ }));
47
+
48
+ jest.mock("@shared/utils", () => ({
49
+ cx: (...args: any[]) => args.filter(Boolean).join(" "),
50
+ }));
51
+
52
+ describe("DesktopLinkGroups", () => {
53
+ it("renders nothing when link is undefined", () => {
54
+ const { container } = render(
55
+ <DesktopLinkGroups anchorName="test" link={undefined} />
56
+ );
57
+ expect(container.innerHTML).toBe("");
58
+ });
59
+
60
+ it("renders single Button when link is a button (has href)", () => {
61
+ const link = {
62
+ href: "/products",
63
+ buttonLabel: "Products",
64
+ anchorId: "prod",
65
+ };
66
+ render(<DesktopLinkGroups anchorName="nav-0" link={link as any} />);
67
+ expect(screen.getByTestId("button")).toHaveTextContent("Products");
68
+ });
69
+
70
+ describe("Group rendering", () => {
71
+ const groupLink = {
72
+ anchorId: "group-1",
73
+ title: "My Account",
74
+ items: {
75
+ items: [
76
+ { buttonLabel: "Profile", href: "/profile", anchorId: "p1" },
77
+ { buttonLabel: "Settings", href: "/settings", anchorId: "p2" },
78
+ ],
79
+ },
80
+ };
81
+
82
+ it("renders group title button", () => {
83
+ render(
84
+ <DesktopLinkGroups anchorName="account-0" link={groupLink as any} />
85
+ );
86
+ expect(screen.getByText("My Account")).toBeInTheDocument();
87
+ });
88
+
89
+ it("renders dropdown arrow icon (down when closed)", () => {
90
+ render(
91
+ <DesktopLinkGroups anchorName="account-0" link={groupLink as any} />
92
+ );
93
+ expect(
94
+ screen.getByTestId("icon-keyboard_arrow_down")
95
+ ).toBeInTheDocument();
96
+ });
97
+
98
+ it("toggles open state and shows up arrow when open", () => {
99
+ render(
100
+ <DesktopLinkGroups anchorName="account-0" link={groupLink as any} />
101
+ );
102
+ const toggleBtn = screen.getAllByTestId("button")[0];
103
+ fireEvent.click(toggleBtn);
104
+ expect(screen.getByTestId("icon-keyboard_arrow_up")).toBeInTheDocument();
105
+ });
106
+
107
+ it("renders submenu items when open", () => {
108
+ render(
109
+ <DesktopLinkGroups anchorName="account-0" link={groupLink as any} />
110
+ );
111
+ const toggleBtn = screen.getAllByTestId("button")[0];
112
+ fireEvent.click(toggleBtn);
113
+ expect(screen.getByText("Profile")).toBeInTheDocument();
114
+ expect(screen.getByText("Settings")).toBeInTheDocument();
115
+ });
116
+
117
+ it("closes when toggled again", () => {
118
+ render(
119
+ <DesktopLinkGroups anchorName="account-0" link={groupLink as any} />
120
+ );
121
+ const toggleBtn = screen.getAllByTestId("button")[0];
122
+ fireEvent.click(toggleBtn);
123
+ fireEvent.click(toggleBtn);
124
+ expect(
125
+ screen.getByTestId("icon-keyboard_arrow_down")
126
+ ).toBeInTheDocument();
127
+ });
128
+
129
+ it("renders null title when title is missing", () => {
130
+ const noTitleLink = { ...groupLink, title: null };
131
+ render(
132
+ <DesktopLinkGroups anchorName="account-0" link={noTitleLink as any} />
133
+ );
134
+ // Should still render without error
135
+ expect(screen.getAllByTestId("button")[0]).toBeInTheDocument();
136
+ });
137
+
138
+ it("handles empty items array", () => {
139
+ const emptyItems = { ...groupLink, items: { items: [] } };
140
+ render(
141
+ <DesktopLinkGroups anchorName="account-0" link={emptyItems as any} />
142
+ );
143
+ const toggleBtn = screen.getAllByTestId("button")[0];
144
+ fireEvent.click(toggleBtn);
145
+ // No submenu links rendered
146
+ expect(screen.getAllByTestId("button")).toHaveLength(1);
147
+ });
148
+
149
+ it("handles null items", () => {
150
+ const nullItems = { ...groupLink, items: null };
151
+ render(
152
+ <DesktopLinkGroups anchorName="account-0" link={nullItems as any} />
153
+ );
154
+ const toggleBtn = screen.getAllByTestId("button")[0];
155
+ fireEvent.click(toggleBtn);
156
+ expect(screen.getAllByTestId("button")).toHaveLength(1);
157
+ });
158
+
159
+ it("closes when a submenu anchor link is clicked", () => {
160
+ render(
161
+ <DesktopLinkGroups anchorName="account-0" link={groupLink as any} />
162
+ );
163
+ const toggleBtn = screen.getAllByTestId("button")[0];
164
+ fireEvent.click(toggleBtn);
165
+ // Simulate clicking an anchor within the submenu ul
166
+ const submenuList = screen.getByRole("list");
167
+ const anchor = document.createElement("a");
168
+ anchor.href = "/profile";
169
+ submenuList.appendChild(anchor);
170
+ fireEvent.click(anchor);
171
+ // After click, arrow should be down (closed)
172
+ expect(
173
+ screen.getByTestId("icon-keyboard_arrow_down")
174
+ ).toBeInTheDocument();
175
+ });
176
+
177
+ it("does not close when non-anchor element is clicked in submenu", () => {
178
+ render(
179
+ <DesktopLinkGroups anchorName="account-0" link={groupLink as any} />
180
+ );
181
+ const toggleBtn = screen.getAllByTestId("button")[0];
182
+ fireEvent.click(toggleBtn);
183
+ const submenuList = screen.getByRole("list");
184
+ // Click on a non-anchor element
185
+ fireEvent.click(submenuList);
186
+ // Should still be open
187
+ expect(screen.getByTestId("icon-keyboard_arrow_up")).toBeInTheDocument();
188
+ });
189
+
190
+ it("closes when outside click is triggered", () => {
191
+ render(
192
+ <DesktopLinkGroups anchorName="account-0" link={groupLink as any} />
193
+ );
194
+ const toggleBtn = screen.getAllByTestId("button")[0];
195
+ fireEvent.click(toggleBtn);
196
+ // Should be open
197
+ expect(screen.getByTestId("icon-keyboard_arrow_up")).toBeInTheDocument();
198
+ // Trigger outside click callback
199
+ act(() => {
200
+ outsideClickCallback!();
201
+ });
202
+ // Should close
203
+ expect(
204
+ screen.getByTestId("icon-keyboard_arrow_down")
205
+ ).toBeInTheDocument();
206
+ });
207
+ });
208
+ });