@windstream/react-shared-components 0.1.92 → 0.1.94

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 (293) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.d.ts +1 -0
  3. package/dist/contentful/index.esm.js +2 -2
  4. package/dist/contentful/index.esm.js.map +1 -1
  5. package/dist/contentful/index.js +3 -3
  6. package/dist/contentful/index.js.map +1 -1
  7. package/dist/core.d.ts +5 -5
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.esm.js +1 -1
  10. package/dist/index.esm.js.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/styles.css +1 -1
  14. package/dist/utils/index.esm.js +1 -1
  15. package/dist/utils/index.js +1 -1
  16. package/package.json +191 -185
  17. package/src/components/accordion/Accordion.stories.tsx +230 -230
  18. package/src/components/accordion/index.test.tsx +270 -0
  19. package/src/components/accordion/index.tsx +70 -70
  20. package/src/components/accordion/types.ts +12 -12
  21. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  22. package/src/components/alert-card/index.test.tsx +152 -0
  23. package/src/components/alert-card/index.tsx +41 -41
  24. package/src/components/alert-card/types.ts +13 -13
  25. package/src/components/animation-wrapper/index.test.tsx +424 -0
  26. package/src/components/animation-wrapper/index.tsx +129 -129
  27. package/src/components/animation-wrapper/types.ts +11 -11
  28. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  29. package/src/components/brand-button/helpers.ts +35 -35
  30. package/src/components/brand-button/index.test.tsx +292 -0
  31. package/src/components/brand-button/index.tsx +120 -120
  32. package/src/components/brand-button/types.ts +38 -38
  33. package/src/components/button/Button.stories.tsx +108 -108
  34. package/src/components/button/index.test.tsx +91 -0
  35. package/src/components/button/index.tsx +27 -27
  36. package/src/components/button/types.ts +14 -14
  37. package/src/components/call-button/CallButton.stories.tsx +324 -324
  38. package/src/components/call-button/index.test.tsx +260 -0
  39. package/src/components/call-button/index.tsx +106 -106
  40. package/src/components/call-button/types.ts +16 -16
  41. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  42. package/src/components/checkbox/index.test.tsx +252 -0
  43. package/src/components/checkbox/index.tsx +197 -197
  44. package/src/components/checkbox/types.ts +27 -27
  45. package/src/components/checklist/Checklist.stories.tsx +150 -150
  46. package/src/components/checklist/index.test.tsx +231 -0
  47. package/src/components/checklist/index.tsx +96 -61
  48. package/src/components/checklist/types.ts +23 -17
  49. package/src/components/collapse/Collapse.stories.tsx +255 -255
  50. package/src/components/collapse/index.test.tsx +277 -0
  51. package/src/components/collapse/index.tsx +47 -46
  52. package/src/components/collapse/types.ts +6 -6
  53. package/src/components/divider/Divider.stories.tsx +205 -205
  54. package/src/components/divider/index.test.tsx +53 -0
  55. package/src/components/divider/index.tsx +22 -22
  56. package/src/components/divider/type.ts +3 -3
  57. package/src/components/image/Image.stories.tsx +113 -113
  58. package/src/components/image/index.test.tsx +174 -0
  59. package/src/components/image/index.tsx +25 -25
  60. package/src/components/image/types.ts +40 -40
  61. package/src/components/input/Input.stories.tsx +325 -325
  62. package/src/components/input/index.test.tsx +348 -0
  63. package/src/components/input/index.tsx +177 -177
  64. package/src/components/input/types.ts +37 -37
  65. package/src/components/link/Link.stories.tsx +163 -163
  66. package/src/components/link/index.test.tsx +199 -0
  67. package/src/components/link/index.tsx +116 -116
  68. package/src/components/link/types.ts +25 -25
  69. package/src/components/list/List.stories.tsx +272 -272
  70. package/src/components/list/index.test.tsx +166 -0
  71. package/src/components/list/index.tsx +88 -88
  72. package/src/components/list/list-item/index.tsx +38 -38
  73. package/src/components/list/list-item/types.ts +13 -13
  74. package/src/components/list/types.ts +29 -29
  75. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  76. package/src/components/material-icon/constants.ts +99 -99
  77. package/src/components/material-icon/index.test.tsx +130 -0
  78. package/src/components/material-icon/index.tsx +47 -47
  79. package/src/components/material-icon/types.ts +31 -31
  80. package/src/components/modal/Modal.stories.tsx +171 -171
  81. package/src/components/modal/index.test.tsx +310 -0
  82. package/src/components/modal/index.tsx +164 -164
  83. package/src/components/modal/types.ts +24 -24
  84. package/src/components/next-image/index.test.tsx +406 -0
  85. package/src/components/next-image/index.tsx +74 -74
  86. package/src/components/next-image/types.ts +1 -1
  87. package/src/components/pagination/index.test.tsx +521 -0
  88. package/src/components/pagination/index.tsx +91 -91
  89. package/src/components/pagination/types.ts +6 -6
  90. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  91. package/src/components/radio-button/index.test.tsx +151 -0
  92. package/src/components/radio-button/index.tsx +75 -75
  93. package/src/components/radio-button/types.ts +21 -21
  94. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  95. package/src/components/see-more/index.test.tsx +96 -0
  96. package/src/components/see-more/index.tsx +44 -44
  97. package/src/components/see-more/types.ts +4 -4
  98. package/src/components/select/Select.stories.tsx +411 -411
  99. package/src/components/select/index.test.tsx +256 -0
  100. package/src/components/select/index.tsx +155 -155
  101. package/src/components/select/types.ts +36 -36
  102. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  103. package/src/components/select-plan-button/index.test.tsx +173 -0
  104. package/src/components/select-plan-button/index.tsx +63 -63
  105. package/src/components/select-plan-button/types.ts +17 -17
  106. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  107. package/src/components/skeleton/index.test.tsx +74 -0
  108. package/src/components/skeleton/index.tsx +61 -61
  109. package/src/components/skeleton/types.ts +4 -4
  110. package/src/components/spinner/Spinner.stories.tsx +335 -335
  111. package/src/components/spinner/index.test.tsx +76 -0
  112. package/src/components/spinner/index.tsx +44 -44
  113. package/src/components/spinner/types.ts +5 -5
  114. package/src/components/text/Text.stories.tsx +321 -321
  115. package/src/components/text/index.test.tsx +65 -0
  116. package/src/components/text/index.tsx +25 -25
  117. package/src/components/text/types.ts +45 -45
  118. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  119. package/src/components/tooltip/index.test.tsx +50 -0
  120. package/src/components/tooltip/index.tsx +74 -74
  121. package/src/components/tooltip/types.ts +7 -7
  122. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  123. package/src/components/view-cart-button/index.test.tsx +57 -0
  124. package/src/components/view-cart-button/index.tsx +42 -42
  125. package/src/components/view-cart-button/types.ts +5 -5
  126. package/src/contentful/blocks/accordion/Accordion.stories.mocks.tsx +128 -128
  127. package/src/contentful/blocks/accordion/Accordion.stories.tsx +98 -98
  128. package/src/contentful/blocks/accordion/index.test.tsx +218 -0
  129. package/src/contentful/blocks/accordion/index.tsx +114 -112
  130. package/src/contentful/blocks/accordion/types.ts +34 -34
  131. package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -0
  132. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  133. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  134. package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -0
  135. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +181 -181
  136. package/src/contentful/blocks/anchored-bottom-banner/types.ts +13 -13
  137. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.mocks.tsx +144 -144
  138. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +157 -156
  139. package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -0
  140. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  141. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  142. package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -0
  143. package/src/contentful/blocks/blogs-grid-base/index.tsx +119 -119
  144. package/src/contentful/blocks/blogs-grid-base/types.ts +36 -36
  145. package/src/contentful/blocks/breadcrumbs/BreadcrumbNavigation.stories.tsx +147 -147
  146. package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -0
  147. package/src/contentful/blocks/breadcrumbs/index.tsx +95 -95
  148. package/src/contentful/blocks/breadcrumbs/types.ts +8 -8
  149. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  150. package/src/contentful/blocks/button/index.test.tsx +339 -0
  151. package/src/contentful/blocks/button/index.tsx +131 -131
  152. package/src/contentful/blocks/button/types.ts +39 -39
  153. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  154. package/src/contentful/blocks/callout/index.test.tsx +539 -0
  155. package/src/contentful/blocks/callout/index.tsx +277 -277
  156. package/src/contentful/blocks/callout/types.ts +78 -78
  157. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  158. package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -0
  159. package/src/contentful/blocks/cards/blog-card/index.tsx +129 -129
  160. package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
  161. package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -0
  162. package/src/contentful/blocks/cards/floating-image-card/index.tsx +119 -119
  163. package/src/contentful/blocks/cards/floating-image-card/types.ts +30 -30
  164. package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -0
  165. package/src/contentful/blocks/cards/full-image-card/index.tsx +130 -130
  166. package/src/contentful/blocks/cards/full-image-card/types.ts +29 -29
  167. package/src/contentful/blocks/cards/index.test.tsx +39 -0
  168. package/src/contentful/blocks/cards/index.tsx +13 -13
  169. package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -0
  170. package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
  171. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  172. package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -0
  173. package/src/contentful/blocks/cards/simple-card/index.tsx +325 -325
  174. package/src/contentful/blocks/cards/simple-card/types.ts +71 -71
  175. package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -0
  176. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  177. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  178. package/src/contentful/blocks/cards/types.ts +1 -1
  179. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  180. package/src/contentful/blocks/carousel/helper.test.tsx +539 -0
  181. package/src/contentful/blocks/carousel/helper.tsx +494 -494
  182. package/src/contentful/blocks/carousel/index.test.tsx +308 -0
  183. package/src/contentful/blocks/carousel/index.tsx +87 -87
  184. package/src/contentful/blocks/carousel/types.test.ts +16 -0
  185. package/src/contentful/blocks/carousel/types.ts +145 -145
  186. package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -0
  187. package/src/contentful/blocks/cart-retention-banner/index.tsx +109 -109
  188. package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
  189. package/src/contentful/blocks/comparison-table/index.test.tsx +114 -0
  190. package/src/contentful/blocks/comparison-table/index.tsx +29 -29
  191. package/src/contentful/blocks/comparison-table/types.ts +6 -6
  192. package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -0
  193. package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
  194. package/src/contentful/blocks/cookiebanner/type.ts +7 -7
  195. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  196. package/src/contentful/blocks/cta-callout/index.test.tsx +244 -0
  197. package/src/contentful/blocks/cta-callout/index.tsx +73 -73
  198. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  199. package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -0
  200. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  201. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  202. package/src/contentful/blocks/email-input-block/index.test.tsx +213 -0
  203. package/src/contentful/blocks/email-input-block/index.tsx +121 -116
  204. package/src/contentful/blocks/email-input-block/types.ts +16 -16
  205. package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
  206. package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -0
  207. package/src/contentful/blocks/find-kinetic/index.tsx +138 -130
  208. package/src/contentful/blocks/find-kinetic/types.ts +20 -19
  209. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  210. package/src/contentful/blocks/floating-banner/index.test.tsx +246 -0
  211. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  212. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  213. package/src/contentful/blocks/footer/Footer.stories.tsx +317 -317
  214. package/src/contentful/blocks/footer/index.test.tsx +302 -0
  215. package/src/contentful/blocks/footer/index.tsx +91 -91
  216. package/src/contentful/blocks/footer/types.ts +13 -13
  217. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  218. package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -0
  219. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  220. package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -0
  221. package/src/contentful/blocks/image-promo-bar/index.tsx +246 -246
  222. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  223. package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -0
  224. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  225. package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -0
  226. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  227. package/src/contentful/blocks/modal/constants.ts +53 -53
  228. package/src/contentful/blocks/modal/index.test.tsx +209 -0
  229. package/src/contentful/blocks/modal/index.tsx +108 -108
  230. package/src/contentful/blocks/modal/types.ts +12 -12
  231. package/src/contentful/blocks/navigation/Navigation.stories.mocks.tsx +78 -0
  232. package/src/contentful/blocks/navigation/Navigation.stories.tsx +138 -0
  233. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -0
  234. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +141 -139
  235. package/src/contentful/blocks/navigation/index.test.tsx +924 -0
  236. package/src/contentful/blocks/navigation/index.tsx +569 -568
  237. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -0
  238. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  239. package/src/contentful/blocks/navigation/types.ts +71 -71
  240. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  241. package/src/contentful/blocks/primary-hero/index.test.tsx +286 -0
  242. package/src/contentful/blocks/primary-hero/index.tsx +239 -236
  243. package/src/contentful/blocks/primary-hero/types.ts +37 -37
  244. package/src/contentful/blocks/search-block/index.test.tsx +268 -0
  245. package/src/contentful/blocks/search-block/index.tsx +90 -90
  246. package/src/contentful/blocks/search-block/types.ts +15 -15
  247. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  248. package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -0
  249. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  250. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  251. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  252. package/src/contentful/blocks/text/index.test.tsx +36 -0
  253. package/src/contentful/blocks/text/index.tsx +12 -12
  254. package/src/contentful/blocks/text/types.ts +1 -1
  255. package/src/contentful/index.test.ts +45 -0
  256. package/src/contentful/index.ts +105 -105
  257. package/src/global-mocks/contentful/to-document.ts +25 -0
  258. package/src/global-mocks/cookie.ts +48 -0
  259. package/src/global-mocks/cx.ts +37 -0
  260. package/src/global-mocks/index.ts +89 -0
  261. package/src/global-mocks/speed-card-bg.ts +27 -0
  262. package/src/global-mocks/utm.ts +49 -0
  263. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -0
  264. package/src/hooks/contentful/use-contentful-rich-text.tsx +309 -309
  265. package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -0
  266. package/src/hooks/contentful/use-processed-check-list.ts +63 -63
  267. package/src/hooks/use-body-scroll-lock.test.ts +134 -0
  268. package/src/hooks/use-body-scroll-lock.ts +34 -34
  269. package/src/hooks/use-carousel-swipe.test.ts +393 -0
  270. package/src/hooks/use-carousel-swipe.ts +264 -264
  271. package/src/hooks/use-outside-click.test.ts +142 -0
  272. package/src/hooks/use-outside-click.ts +17 -17
  273. package/src/index.ts +107 -107
  274. package/src/next/index.test.ts +7 -0
  275. package/src/next/index.ts +5 -5
  276. package/src/setupTests.ts +52 -46
  277. package/src/stories/DocsTemplate.tsx +24 -24
  278. package/src/styles/globals.css +343 -343
  279. package/src/types/global.d.ts +9 -9
  280. package/src/types/micro-components.ts +99 -99
  281. package/src/types/utm.ts +49 -49
  282. package/src/utils/contentful/to-document.test.ts +85 -0
  283. package/src/utils/contentful/to-document.ts +24 -24
  284. package/src/utils/cookie.test.ts +180 -0
  285. package/src/utils/cookie.ts +84 -84
  286. package/src/utils/cx.test.ts +90 -0
  287. package/src/utils/cx.ts +49 -49
  288. package/src/utils/index.test.ts +115 -0
  289. package/src/utils/index.ts +41 -41
  290. package/src/utils/speed-card-bg.test.ts +46 -0
  291. package/src/utils/speed-card-bg.ts +24 -24
  292. package/src/utils/utm.test.ts +359 -0
  293. package/src/utils/utm.ts +221 -221
@@ -0,0 +1,302 @@
1
+ import React from "react";
2
+ import { Footer } from "./index";
3
+ import { FooterProps } from "./types";
4
+
5
+ import { fireEvent, render, screen } from "@testing-library/react";
6
+
7
+ // Mock dependencies
8
+ jest.mock("@shared/components/divider", () => ({
9
+ Divider: ({ className }: { className?: string }) => (
10
+ <hr data-testid="divider" className={className} />
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("../button", () => ({
21
+ Button: ({
22
+ onClick,
23
+ linkClassName,
24
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
25
+ linkVariant,
26
+ buttonLabel,
27
+ ...rest
28
+ }: any) => (
29
+ <button
30
+ data-testid="footer-button"
31
+ className={linkClassName}
32
+ onClick={onClick}
33
+ >
34
+ {buttonLabel || rest.children || "Link"}
35
+ </button>
36
+ ),
37
+ }));
38
+
39
+ const mockLinks = [
40
+ {
41
+ title: "Products",
42
+ items: {
43
+ items: [
44
+ { buttonLabel: "Internet", href: "/internet" },
45
+ { buttonLabel: "TV", href: "/tv" },
46
+ { buttonLabel: "Phone", href: "/phone" },
47
+ ],
48
+ },
49
+ },
50
+ {
51
+ title: "Support",
52
+ items: {
53
+ items: [
54
+ { buttonLabel: "Help Center", href: "/help" },
55
+ { buttonLabel: "Contact Us", href: "/contact" },
56
+ ],
57
+ },
58
+ },
59
+ ];
60
+
61
+ const mockBottomLinks = [
62
+ { buttonLabel: "Privacy Policy", href: "/privacy" },
63
+ { buttonLabel: "Terms of Use", href: "/terms" },
64
+ { buttonLabel: "Accessibility", href: "/accessibility" },
65
+ ];
66
+
67
+ const defaultProps: FooterProps = {
68
+ body: <p>Promotional content and disclaimers go here.</p>,
69
+ links: mockLinks,
70
+ bottomLinks: mockBottomLinks,
71
+ copyrights: "All rights reserved.",
72
+ terms: "Kinetic by Windstream.",
73
+ maxWidth: true,
74
+ };
75
+
76
+ describe("Footer", () => {
77
+ describe("Rendering", () => {
78
+ it("renders with all props", () => {
79
+ render(<Footer {...defaultProps} />);
80
+ expect(
81
+ screen.getByText("Promotional content and disclaimers go here.")
82
+ ).toBeInTheDocument();
83
+ expect(screen.getByText("Products")).toBeInTheDocument();
84
+ expect(screen.getByText("Support")).toBeInTheDocument();
85
+ expect(screen.getByText("All rights reserved.")).toBeInTheDocument();
86
+ });
87
+
88
+ it("renders with minimal props (body only)", () => {
89
+ render(<Footer body={<span>Minimal body</span>} />);
90
+ expect(screen.getByText("Minimal body")).toBeInTheDocument();
91
+ });
92
+
93
+ it("renders body content in aside with proper aria-label", () => {
94
+ render(<Footer {...defaultProps} />);
95
+ const aside = screen.getByLabelText("Promotions and disclaimers");
96
+ expect(aside.tagName).toBe("ASIDE");
97
+ expect(aside).toHaveClass("break-words");
98
+ });
99
+ });
100
+
101
+ describe("Promotional section (body)", () => {
102
+ it("renders ReactNode body content", () => {
103
+ render(
104
+ <Footer
105
+ body={
106
+ <div data-testid="promo">
107
+ <strong>Big Sale!</strong> Save 50%
108
+ </div>
109
+ }
110
+ />
111
+ );
112
+ expect(screen.getByTestId("promo")).toBeInTheDocument();
113
+ expect(screen.getByText("Save 50%")).toBeInTheDocument();
114
+ });
115
+
116
+ it("renders plain text body", () => {
117
+ render(<Footer body="Simple promotional text" />);
118
+ expect(screen.getByText("Simple promotional text")).toBeInTheDocument();
119
+ });
120
+ });
121
+
122
+ describe("Footer link groups", () => {
123
+ it("renders link group titles", () => {
124
+ render(<Footer {...defaultProps} />);
125
+ expect(screen.getByText("Products")).toBeInTheDocument();
126
+ expect(screen.getByText("Support")).toBeInTheDocument();
127
+ });
128
+
129
+ it("renders all link items within groups", () => {
130
+ render(<Footer {...defaultProps} />);
131
+ const buttons = screen.getAllByTestId("footer-button");
132
+ // 3 Products links + 2 Support links + 3 bottom links = 8
133
+ expect(buttons).toHaveLength(8);
134
+ });
135
+
136
+ it("renders nav elements with proper aria-labelledby", () => {
137
+ const { container } = render(<Footer {...defaultProps} />);
138
+ const navs = container.querySelectorAll("nav.site-links-group");
139
+ expect(navs).toHaveLength(2);
140
+ expect(navs[0]).toHaveAttribute("aria-labelledby", "footer-Products");
141
+ expect(navs[1]).toHaveAttribute("aria-labelledby", "footer-Support");
142
+ });
143
+
144
+ it("renders no link groups when links is undefined", () => {
145
+ render(<Footer body="No links" />);
146
+ expect(screen.queryByText("Products")).not.toBeInTheDocument();
147
+ });
148
+
149
+ it("renders no link groups when links is empty", () => {
150
+ render(<Footer body="No links" links={[]} />);
151
+ expect(screen.queryByText("Products")).not.toBeInTheDocument();
152
+ });
153
+
154
+ it("handles link groups with missing title and items gracefully", () => {
155
+ const partialLinks = [
156
+ { title: undefined, items: undefined },
157
+ { title: "Valid", items: { items: undefined } },
158
+ { title: "Also Valid", items: { items: [] } },
159
+ ];
160
+ const { container } = render(
161
+ <Footer body="Partial" links={partialLinks} />
162
+ );
163
+ const navs = container.querySelectorAll("nav.site-links-group");
164
+ expect(navs).toHaveLength(3);
165
+ expect(screen.getByText("Valid")).toBeInTheDocument();
166
+ expect(screen.getByText("Also Valid")).toBeInTheDocument();
167
+ });
168
+
169
+ it("renders dividers between link groups on mobile", () => {
170
+ render(<Footer {...defaultProps} />);
171
+ const dividers = screen.getAllByTestId("divider");
172
+ // 1 main divider + 2 mobile dividers (one per link group) + 1 bottom divider = 4
173
+ expect(dividers.length).toBeGreaterThanOrEqual(3);
174
+ });
175
+ });
176
+
177
+ describe("Footer layout", () => {
178
+ it("renders with 4-column grid layout", () => {
179
+ const { container } = render(<Footer {...defaultProps} />);
180
+ const grid = container.querySelector(".lg\\:grid-cols-4");
181
+ expect(grid).toBeInTheDocument();
182
+ });
183
+
184
+ it("applies inverse background and white text", () => {
185
+ const { container } = render(<Footer {...defaultProps} />);
186
+ expect(container.firstChild).toHaveClass("bg-bg-inverse");
187
+ expect(container.firstChild).toHaveClass("text-white");
188
+ });
189
+ });
190
+
191
+ describe("Max width", () => {
192
+ it("applies max-w-120 when maxWidth is true (default)", () => {
193
+ const { container } = render(<Footer {...defaultProps} />);
194
+ const wrapper = container.querySelector(".max-w-120");
195
+ expect(wrapper).toBeInTheDocument();
196
+ });
197
+
198
+ it("does not apply max-w-120 when maxWidth is false", () => {
199
+ const { container } = render(
200
+ <Footer {...defaultProps} maxWidth={false} />
201
+ );
202
+ const wrapper = container.querySelector(".max-w-120");
203
+ expect(wrapper).not.toBeInTheDocument();
204
+ });
205
+ });
206
+
207
+ describe("Copyright and terms", () => {
208
+ it("renders copyright year with current year", () => {
209
+ render(<Footer {...defaultProps} />);
210
+ const currentYear = new Date().getFullYear().toString();
211
+ const termsEl = screen.getByText(
212
+ content =>
213
+ content.includes(currentYear) &&
214
+ content.includes("Kinetic by Windstream.")
215
+ );
216
+ expect(termsEl).toBeInTheDocument();
217
+ });
218
+
219
+ it("renders copyrights text", () => {
220
+ render(<Footer {...defaultProps} />);
221
+ expect(screen.getByText("All rights reserved.")).toBeInTheDocument();
222
+ });
223
+
224
+ it("renders terms with copyright symbol", () => {
225
+ render(<Footer {...defaultProps} />);
226
+ const termsEl = screen.getByText(/©.*Kinetic by Windstream/);
227
+ expect(termsEl).toBeInTheDocument();
228
+ expect(termsEl).toHaveClass("terms-text");
229
+ });
230
+
231
+ it("renders copyrights with whitespace-pre-line class", () => {
232
+ render(<Footer {...defaultProps} />);
233
+ const copyEl = screen.getByText("All rights reserved.");
234
+ expect(copyEl).toHaveClass("copy-rights");
235
+ expect(copyEl).toHaveClass("whitespace-pre-line");
236
+ });
237
+ });
238
+
239
+ describe("Bottom links", () => {
240
+ it("renders bottom links", () => {
241
+ render(<Footer {...defaultProps} />);
242
+ const legalNav = screen.getByLabelText("Footer legal");
243
+ expect(legalNav).toBeInTheDocument();
244
+ });
245
+
246
+ it("renders correct number of bottom links", () => {
247
+ render(<Footer {...defaultProps} />);
248
+ const legalNav = screen.getByLabelText("Footer legal");
249
+ const items = legalNav.querySelectorAll("li");
250
+ expect(items).toHaveLength(3);
251
+ });
252
+
253
+ it("renders no bottom links when not provided", () => {
254
+ render(<Footer body="No bottom" />);
255
+ const legalNav = screen.getByLabelText("Footer legal");
256
+ const items = legalNav.querySelectorAll("li");
257
+ expect(items).toHaveLength(0);
258
+ });
259
+ });
260
+
261
+ describe("onClick handler", () => {
262
+ it("calls onClick when a site link is clicked", () => {
263
+ const handleClick = jest.fn();
264
+ render(<Footer {...defaultProps} onClick={handleClick} />);
265
+ const buttons = screen.getAllByTestId("footer-button");
266
+ fireEvent.click(buttons[0]);
267
+ expect(handleClick).toHaveBeenCalledTimes(1);
268
+ });
269
+
270
+ it("calls onClick for each link clicked", () => {
271
+ const handleClick = jest.fn();
272
+ render(<Footer {...defaultProps} onClick={handleClick} />);
273
+ const buttons = screen.getAllByTestId("footer-button");
274
+ fireEvent.click(buttons[0]);
275
+ fireEvent.click(buttons[1]);
276
+ expect(handleClick).toHaveBeenCalledTimes(2);
277
+ });
278
+
279
+ it("does not error when onClick is not provided", () => {
280
+ render(<Footer body="No click handler" links={mockLinks} />);
281
+ const buttons = screen.getAllByTestId("footer-button");
282
+ expect(() => fireEvent.click(buttons[0])).not.toThrow();
283
+ });
284
+ });
285
+
286
+ describe("Dividers", () => {
287
+ it("renders main divider between body and links", () => {
288
+ render(<Footer {...defaultProps} />);
289
+ const dividers = screen.getAllByTestId("divider");
290
+ expect(dividers.length).toBeGreaterThanOrEqual(1);
291
+ });
292
+
293
+ it("renders dividers with inverse border class", () => {
294
+ render(<Footer {...defaultProps} />);
295
+ const dividers = screen.getAllByTestId("divider");
296
+ const inverseDividers = dividers.filter(d =>
297
+ d.className.includes("border-border-inverse")
298
+ );
299
+ expect(inverseDividers.length).toBeGreaterThanOrEqual(1);
300
+ });
301
+ });
302
+ });
@@ -1,91 +1,91 @@
1
- import React from "react";
2
- import { Button } from "../button";
3
- import { FooterProps } from "./types";
4
-
5
- import { Divider } from "@shared/components/divider";
6
- import { Text } from "@shared/components/text";
7
-
8
- export const Footer: React.FC<FooterProps> = ({
9
- body,
10
- links,
11
- bottomLinks,
12
- copyrights,
13
- terms,
14
- maxWidth = true,
15
- onClick,
16
- }) => {
17
- function footerClick(
18
- event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
19
- ): void {
20
- onClick?.(event);
21
- }
22
-
23
- return (
24
- <div className="component-container bg-bg-inverse text-white">
25
- <div className={` ${maxWidth ? "mx-auto max-w-120" : ""} px-5 py-8`}>
26
- <aside className="break-words" aria-label="Promotions and disclaimers">
27
- {body}
28
- </aside>
29
- <Divider className="my-8 border-border-inverse lg:my-16" />
30
- <div className="grid-1 grid gap-8 md:grid-cols-2 lg:grid-cols-4">
31
- {links?.map((link: any, index: number) => (
32
- <React.Fragment key={`footer-link-group-${index}`}>
33
- <div key={`footer-link-group-${index}`}>
34
- <nav
35
- aria-labelledby={`footer-${link?.title}`}
36
- className="site-links-group"
37
- key={`link-group-${index}`}
38
- >
39
- <Text as="div" className="label3">
40
- {link?.title}
41
- </Text>
42
- <ul className="flex flex-col">
43
- {link?.items?.items?.map((site: any, _index: number) => (
44
- <li className="pt-3" key={`site-links-${_index}`}>
45
- <Button
46
- {...site}
47
- linkVariant="unstyled"
48
- linkClassName="text-white body3 hover:underline underline-offset-4"
49
- onClick={footerClick}
50
- />
51
- </li>
52
- ))}
53
- </ul>
54
- </nav>
55
- </div>
56
- <Divider className="border-border-inverse md:hidden" />
57
- </React.Fragment>
58
- ))}
59
- </div>
60
-
61
- <div className="py-8">
62
- <Divider className="my-8 hidden border-border-inverse md:my-8 md:block" />
63
-
64
- <p className="terms-text footnote text-white">
65
- &copy; {new Date().getFullYear()} {terms}
66
- </p>
67
- <p className="copy-rights whitespace-pre-line">{copyrights}</p>
68
- <nav aria-label="Footer legal">
69
- <ul className="bottom-links list-none">
70
- {bottomLinks?.map((link: any, index: number) => (
71
- <li
72
- key={`links-${index}`}
73
- className="relative float-left pl-[8px] before:absolute before:left-[3px] before:top-1/2 before:-translate-y-1/2 before:text-[14px] before:content-['•'] first-of-type:pl-0 first-of-type:before:content-['']"
74
- >
75
- <Button
76
- {...link}
77
- linkClassName="footnote text-white hover:underline underline-offset-4"
78
- linkVariant="unstyled"
79
- onClick={footerClick}
80
- />
81
- </li>
82
- ))}
83
- </ul>
84
- </nav>
85
- </div>
86
- </div>
87
- </div>
88
- );
89
- };
90
-
91
- export default Footer;
1
+ import React from "react";
2
+ import { Button } from "../button";
3
+ import { FooterProps } from "./types";
4
+
5
+ import { Divider } from "@shared/components/divider";
6
+ import { Text } from "@shared/components/text";
7
+
8
+ export const Footer: React.FC<FooterProps> = ({
9
+ body,
10
+ links,
11
+ bottomLinks,
12
+ copyrights,
13
+ terms,
14
+ maxWidth = true,
15
+ onClick,
16
+ }) => {
17
+ function footerClick(
18
+ event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
19
+ ): void {
20
+ onClick?.(event);
21
+ }
22
+
23
+ return (
24
+ <div className="component-container bg-bg-inverse text-white">
25
+ <div className={` ${maxWidth ? "mx-auto max-w-120" : ""} px-5 py-8`}>
26
+ <aside className="break-words" aria-label="Promotions and disclaimers">
27
+ {body}
28
+ </aside>
29
+ <Divider className="my-8 border-border-inverse lg:my-16" />
30
+ <div className="grid-1 grid gap-8 md:grid-cols-2 lg:grid-cols-4">
31
+ {links?.map((link: any, index: number) => (
32
+ <React.Fragment key={`footer-link-group-${index}`}>
33
+ <div key={`footer-link-group-${index}`}>
34
+ <nav
35
+ aria-labelledby={`footer-${link?.title}`}
36
+ className="site-links-group"
37
+ key={`link-group-${index}`}
38
+ >
39
+ <Text as="div" className="label3">
40
+ {link?.title}
41
+ </Text>
42
+ <ul className="flex flex-col">
43
+ {link?.items?.items?.map((site: any, _index: number) => (
44
+ <li className="pt-3" key={`site-links-${_index}`}>
45
+ <Button
46
+ {...site}
47
+ linkVariant="unstyled"
48
+ linkClassName="text-white body3 hover:underline underline-offset-4"
49
+ onClick={footerClick}
50
+ />
51
+ </li>
52
+ ))}
53
+ </ul>
54
+ </nav>
55
+ </div>
56
+ <Divider className="border-border-inverse md:hidden" />
57
+ </React.Fragment>
58
+ ))}
59
+ </div>
60
+
61
+ <div className="py-8">
62
+ <Divider className="my-8 hidden border-border-inverse md:my-8 md:block" />
63
+
64
+ <p className="terms-text footnote text-white">
65
+ &copy; {new Date().getFullYear()} {terms}
66
+ </p>
67
+ <p className="copy-rights whitespace-pre-line">{copyrights}</p>
68
+ <nav aria-label="Footer legal">
69
+ <ul className="bottom-links list-none">
70
+ {bottomLinks?.map((link: any, index: number) => (
71
+ <li
72
+ key={`links-${index}`}
73
+ className="relative float-left pl-[8px] before:absolute before:left-[3px] before:top-1/2 before:-translate-y-1/2 before:text-[14px] before:content-['•'] first-of-type:pl-0 first-of-type:before:content-['']"
74
+ >
75
+ <Button
76
+ {...link}
77
+ linkClassName="footnote text-white hover:underline underline-offset-4"
78
+ linkVariant="unstyled"
79
+ onClick={footerClick}
80
+ />
81
+ </li>
82
+ ))}
83
+ </ul>
84
+ </nav>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ );
89
+ };
90
+
91
+ export default Footer;
@@ -1,13 +1,13 @@
1
- import React from "react";
2
-
3
- export type FooterProps = {
4
- body: React.ReactNode;
5
- links?: Array<any>;
6
- bottomLinks?: Array<any>;
7
- copyrights?: React.ReactNode;
8
- terms?: string;
9
- maxWidth?: boolean;
10
- onClick?: (
11
- event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
12
- ) => void;
13
- };
1
+ import React from "react";
2
+
3
+ export type FooterProps = {
4
+ body: React.ReactNode;
5
+ links?: Array<any>;
6
+ bottomLinks?: Array<any>;
7
+ copyrights?: React.ReactNode;
8
+ terms?: string;
9
+ maxWidth?: boolean;
10
+ onClick?: (
11
+ event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
12
+ ) => void;
13
+ };
@@ -1,23 +1,23 @@
1
- import { ImagePromoBar } from "./index";
2
-
3
- import { DocsPage } from "@shared/stories/DocsTemplate";
4
- import type { Meta, StoryObj } from "@storybook/react";
5
-
6
- const meta: Meta<typeof ImagePromoBar> = {
7
- title: "Contentful Blocks/ImagePromoBar",
8
- component: ImagePromoBar,
9
- tags: ["autodocs"],
10
- parameters: {
11
- layout: "centered",
12
- docs: {
13
- page: DocsPage,
14
- description: {
15
- component: "Contentful image promo bar block.",
16
- },
17
- },
18
- },
19
- args: {},
20
- };
21
- export default meta;
22
- type Story = StoryObj<typeof meta>;
23
- export const Default: Story = {};
1
+ import { ImagePromoBar } from "./index";
2
+
3
+ import { DocsPage } from "@shared/stories/DocsTemplate";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+
6
+ const meta: Meta<typeof ImagePromoBar> = {
7
+ title: "Contentful Blocks/ImagePromoBar",
8
+ component: ImagePromoBar,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ layout: "centered",
12
+ docs: {
13
+ page: DocsPage,
14
+ description: {
15
+ component: "Contentful image promo bar block.",
16
+ },
17
+ },
18
+ },
19
+ args: {},
20
+ };
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+ export const Default: Story = {};
@@ -0,0 +1,61 @@
1
+ import React from "react";
2
+ import { PlayButton } from "./helper";
3
+
4
+ import { render, screen } from "@testing-library/react";
5
+
6
+ jest.mock("@shared/components/material-icon", () => ({
7
+ MaterialIcon: ({ name, size, fill, className }: any) => (
8
+ <span
9
+ data-testid="material-icon"
10
+ data-name={name}
11
+ data-size={size}
12
+ data-fill={fill}
13
+ className={className}
14
+ />
15
+ ),
16
+ }));
17
+
18
+ describe("PlayButton", () => {
19
+ it("renders button with aria-label", () => {
20
+ render(<PlayButton />);
21
+ expect(
22
+ screen.getByRole("button", { name: "Play button" })
23
+ ).toBeInTheDocument();
24
+ });
25
+
26
+ it("renders MaterialIcon with play_arrow", () => {
27
+ render(<PlayButton />);
28
+ expect(screen.getByTestId("material-icon")).toHaveAttribute(
29
+ "data-name",
30
+ "play_arrow"
31
+ );
32
+ });
33
+
34
+ it("applies text-text-brand when isHovered is true", () => {
35
+ render(<PlayButton isHovered={true} />);
36
+ expect(screen.getByTestId("material-icon").className).toContain(
37
+ "text-text-brand"
38
+ );
39
+ expect(screen.getByTestId("material-icon").className).not.toContain(
40
+ "text-slate-500"
41
+ );
42
+ });
43
+
44
+ it("applies text-slate-500 when isHovered is false", () => {
45
+ render(<PlayButton isHovered={false} />);
46
+ expect(screen.getByTestId("material-icon").className).toContain(
47
+ "text-slate-500"
48
+ );
49
+ });
50
+
51
+ it("applies containerClassName when provided", () => {
52
+ render(<PlayButton containerClassName="custom-class" />);
53
+ expect(screen.getByRole("button").className).toContain("custom-class");
54
+ });
55
+
56
+ it("does not add extra class when containerClassName is undefined", () => {
57
+ const { container } = render(<PlayButton />);
58
+ const btn = container.querySelector("button");
59
+ expect(btn?.className).not.toContain("undefined");
60
+ });
61
+ });
@@ -1,28 +1,28 @@
1
- import React from "react";
2
- import { PlayButtonProps } from "./types";
3
-
4
- import { MaterialIcon } from "@shared/components/material-icon";
5
-
6
- export const PlayButton: React.FC<PlayButtonProps> = ({
7
- isHovered: isOuterHovered,
8
- containerClassName,
9
- }) => {
10
- return (
11
- <button
12
- tabIndex={-1}
13
- className={`group flex h-20 w-20 items-center justify-center rounded-full bg-white shadow-xl transition-all duration-300 hover:scale-110 active:scale-95 ${containerClassName || ""}`}
14
- aria-label="Play button"
15
- >
16
- <MaterialIcon
17
- name="play_arrow"
18
- size={72 as any}
19
- fill={1}
20
- className={`transition-colors duration-300 ${
21
- isOuterHovered
22
- ? "text-text-brand"
23
- : "text-slate-500 group-hover:text-text-brand"
24
- }`}
25
- />
26
- </button>
27
- );
28
- };
1
+ import React from "react";
2
+ import { PlayButtonProps } from "./types";
3
+
4
+ import { MaterialIcon } from "@shared/components/material-icon";
5
+
6
+ export const PlayButton: React.FC<PlayButtonProps> = ({
7
+ isHovered: isOuterHovered,
8
+ containerClassName,
9
+ }) => {
10
+ return (
11
+ <button
12
+ tabIndex={-1}
13
+ className={`group flex h-20 w-20 items-center justify-center rounded-full bg-white shadow-xl transition-all duration-300 hover:scale-110 active:scale-95 ${containerClassName || ""}`}
14
+ aria-label="Play button"
15
+ >
16
+ <MaterialIcon
17
+ name="play_arrow"
18
+ size={72 as any}
19
+ fill={1}
20
+ className={`transition-colors duration-300 ${
21
+ isOuterHovered
22
+ ? "text-text-brand"
23
+ : "text-slate-500 group-hover:text-text-brand"
24
+ }`}
25
+ />
26
+ </button>
27
+ );
28
+ };