@windstream/react-shared-components 0.1.94 → 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.
- package/README.md +635 -635
- package/dist/contentful/index.esm.js +3 -3
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +3 -3
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +4 -4
- package/dist/index.esm.js +5 -13
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +5 -13
- package/dist/index.js.map +1 -1
- package/dist/next/index.esm.js +2 -2
- package/dist/next/index.esm.js.map +1 -1
- package/dist/next/index.js +2 -2
- package/dist/next/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.esm.js.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +191 -191
- package/src/components/accordion/Accordion.stories.tsx +230 -230
- package/src/components/accordion/index.test.tsx +270 -270
- package/src/components/accordion/index.tsx +70 -70
- package/src/components/accordion/types.ts +12 -12
- package/src/components/alert-card/AlertCard.stories.tsx +171 -171
- package/src/components/alert-card/index.test.tsx +152 -152
- package/src/components/alert-card/index.tsx +41 -41
- package/src/components/alert-card/types.ts +13 -13
- package/src/components/animation-wrapper/index.test.tsx +424 -424
- package/src/components/animation-wrapper/index.tsx +129 -129
- package/src/components/animation-wrapper/types.ts +11 -11
- package/src/components/brand-button/BrandButton.stories.tsx +223 -223
- package/src/components/brand-button/helpers.ts +35 -35
- package/src/components/brand-button/index.test.tsx +292 -292
- package/src/components/brand-button/index.tsx +120 -120
- package/src/components/brand-button/types.ts +38 -38
- package/src/components/button/Button.stories.tsx +108 -108
- package/src/components/button/index.test.tsx +91 -91
- package/src/components/button/index.tsx +27 -27
- package/src/components/button/types.ts +14 -14
- package/src/components/call-button/CallButton.stories.tsx +324 -324
- package/src/components/call-button/index.test.tsx +260 -260
- package/src/components/call-button/index.tsx +106 -106
- package/src/components/call-button/types.ts +16 -16
- package/src/components/checkbox/Checkbox.stories.tsx +247 -247
- package/src/components/checkbox/index.test.tsx +252 -252
- package/src/components/checkbox/index.tsx +197 -197
- package/src/components/checkbox/types.ts +27 -27
- package/src/components/checklist/Checklist.stories.tsx +150 -150
- package/src/components/checklist/index.test.tsx +231 -231
- package/src/components/checklist/index.tsx +96 -96
- package/src/components/checklist/types.ts +23 -23
- package/src/components/collapse/Collapse.stories.tsx +255 -255
- package/src/components/collapse/index.test.tsx +277 -277
- package/src/components/collapse/index.tsx +47 -47
- package/src/components/collapse/types.ts +6 -6
- package/src/components/divider/Divider.stories.tsx +205 -205
- package/src/components/divider/index.test.tsx +53 -53
- package/src/components/divider/index.tsx +22 -22
- package/src/components/divider/type.ts +3 -3
- package/src/components/image/Image.stories.tsx +113 -113
- package/src/components/image/index.test.tsx +174 -174
- package/src/components/image/index.tsx +25 -25
- package/src/components/image/types.ts +40 -40
- package/src/components/input/Input.stories.tsx +325 -325
- package/src/components/input/index.test.tsx +348 -348
- package/src/components/input/index.tsx +177 -177
- package/src/components/input/types.ts +37 -37
- package/src/components/link/Link.stories.tsx +163 -163
- package/src/components/link/index.test.tsx +199 -199
- package/src/components/link/index.tsx +116 -116
- package/src/components/link/types.ts +25 -25
- package/src/components/list/List.stories.tsx +272 -272
- package/src/components/list/index.test.tsx +166 -166
- package/src/components/list/index.tsx +88 -88
- package/src/components/list/list-item/index.tsx +38 -38
- package/src/components/list/list-item/types.ts +13 -13
- package/src/components/list/types.ts +29 -29
- package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
- package/src/components/material-icon/constants.ts +99 -99
- package/src/components/material-icon/index.test.tsx +130 -130
- package/src/components/material-icon/index.tsx +47 -47
- package/src/components/material-icon/types.ts +31 -31
- package/src/components/modal/Modal.stories.tsx +171 -171
- package/src/components/modal/index.test.tsx +310 -310
- package/src/components/modal/index.tsx +164 -164
- package/src/components/modal/types.ts +24 -24
- package/src/components/next-image/index.test.tsx +406 -406
- package/src/components/next-image/index.tsx +74 -74
- package/src/components/next-image/types.ts +1 -1
- package/src/components/pagination/index.test.tsx +521 -521
- package/src/components/pagination/index.tsx +91 -91
- package/src/components/pagination/types.ts +6 -6
- package/src/components/radio-button/RadioButton.stories.tsx +307 -307
- package/src/components/radio-button/index.test.tsx +151 -151
- package/src/components/radio-button/index.tsx +75 -75
- package/src/components/radio-button/types.ts +21 -21
- package/src/components/see-more/SeeMore.stories.tsx +181 -181
- package/src/components/see-more/index.test.tsx +96 -96
- package/src/components/see-more/index.tsx +44 -44
- package/src/components/see-more/types.ts +4 -4
- package/src/components/select/Select.stories.tsx +411 -411
- package/src/components/select/index.test.tsx +256 -256
- package/src/components/select/index.tsx +155 -155
- package/src/components/select/types.ts +36 -36
- package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
- package/src/components/select-plan-button/index.test.tsx +173 -173
- package/src/components/select-plan-button/index.tsx +63 -63
- package/src/components/select-plan-button/types.ts +17 -17
- package/src/components/skeleton/Skeleton.stories.tsx +179 -179
- package/src/components/skeleton/index.test.tsx +74 -74
- package/src/components/skeleton/index.tsx +61 -61
- package/src/components/skeleton/types.ts +4 -4
- package/src/components/spinner/Spinner.stories.tsx +335 -335
- package/src/components/spinner/index.test.tsx +76 -76
- package/src/components/spinner/index.tsx +44 -44
- package/src/components/spinner/types.ts +5 -5
- package/src/components/text/Text.stories.tsx +321 -321
- package/src/components/text/index.test.tsx +65 -65
- package/src/components/text/index.tsx +25 -25
- package/src/components/text/types.ts +45 -45
- package/src/components/tooltip/Tooltip.stories.tsx +219 -219
- package/src/components/tooltip/index.test.tsx +50 -50
- package/src/components/tooltip/index.tsx +74 -74
- package/src/components/tooltip/types.ts +7 -7
- package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
- package/src/components/view-cart-button/index.test.tsx +57 -57
- package/src/components/view-cart-button/index.tsx +42 -42
- package/src/components/view-cart-button/types.ts +5 -5
- package/src/contentful/blocks/accordion/Accordion.stories.mocks.tsx +128 -128
- package/src/contentful/blocks/accordion/Accordion.stories.tsx +98 -98
- package/src/contentful/blocks/accordion/index.test.tsx +218 -218
- package/src/contentful/blocks/accordion/index.tsx +114 -114
- package/src/contentful/blocks/accordion/types.ts +34 -34
- package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -132
- package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
- package/src/contentful/blocks/address-input-banner/types.ts +14 -14
- package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -287
- package/src/contentful/blocks/anchored-bottom-banner/index.tsx +181 -181
- package/src/contentful/blocks/anchored-bottom-banner/types.ts +13 -13
- package/src/contentful/blocks/blogs-grid/BlogGrid.stories.mocks.tsx +144 -144
- package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +157 -157
- package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -355
- package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
- package/src/contentful/blocks/blogs-grid/types.ts +26 -26
- package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -274
- package/src/contentful/blocks/blogs-grid-base/index.tsx +119 -119
- package/src/contentful/blocks/blogs-grid-base/types.ts +36 -36
- package/src/contentful/blocks/breadcrumbs/BreadcrumbNavigation.stories.tsx +147 -147
- package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -281
- package/src/contentful/blocks/breadcrumbs/index.tsx +95 -95
- package/src/contentful/blocks/breadcrumbs/types.ts +8 -8
- package/src/contentful/blocks/button/Button.stories.tsx +40 -40
- package/src/contentful/blocks/button/index.test.tsx +339 -339
- package/src/contentful/blocks/button/index.tsx +131 -131
- package/src/contentful/blocks/button/types.ts +39 -39
- package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
- package/src/contentful/blocks/callout/index.test.tsx +539 -539
- package/src/contentful/blocks/callout/index.tsx +277 -277
- package/src/contentful/blocks/callout/types.ts +78 -78
- package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
- package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -218
- package/src/contentful/blocks/cards/blog-card/index.tsx +129 -129
- package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
- package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -201
- package/src/contentful/blocks/cards/floating-image-card/index.tsx +119 -119
- package/src/contentful/blocks/cards/floating-image-card/types.ts +30 -30
- package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -216
- package/src/contentful/blocks/cards/full-image-card/index.tsx +130 -130
- package/src/contentful/blocks/cards/full-image-card/types.ts +29 -29
- package/src/contentful/blocks/cards/index.test.tsx +39 -39
- package/src/contentful/blocks/cards/index.tsx +13 -13
- package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -263
- package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
- package/src/contentful/blocks/cards/product-card/types.ts +28 -28
- package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -364
- package/src/contentful/blocks/cards/simple-card/index.tsx +325 -325
- package/src/contentful/blocks/cards/simple-card/types.ts +71 -71
- package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -180
- package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
- package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
- package/src/contentful/blocks/cards/types.ts +1 -1
- package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
- package/src/contentful/blocks/carousel/helper.test.tsx +539 -539
- package/src/contentful/blocks/carousel/helper.tsx +494 -494
- package/src/contentful/blocks/carousel/index.test.tsx +308 -308
- package/src/contentful/blocks/carousel/index.tsx +87 -87
- package/src/contentful/blocks/carousel/types.test.ts +16 -16
- package/src/contentful/blocks/carousel/types.ts +145 -145
- package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -409
- package/src/contentful/blocks/cart-retention-banner/index.tsx +109 -109
- package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
- package/src/contentful/blocks/comparison-table/index.test.tsx +114 -114
- package/src/contentful/blocks/comparison-table/index.tsx +29 -29
- package/src/contentful/blocks/comparison-table/types.ts +6 -6
- package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -277
- package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
- package/src/contentful/blocks/cookiebanner/type.ts +7 -7
- package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
- package/src/contentful/blocks/cta-callout/index.test.tsx +244 -244
- package/src/contentful/blocks/cta-callout/index.tsx +73 -73
- package/src/contentful/blocks/cta-callout/types.ts +26 -26
- package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -240
- package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
- package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
- package/src/contentful/blocks/email-input-block/index.test.tsx +213 -213
- package/src/contentful/blocks/email-input-block/index.tsx +121 -121
- package/src/contentful/blocks/email-input-block/types.ts +16 -16
- package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
- package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -269
- package/src/contentful/blocks/find-kinetic/index.tsx +138 -138
- package/src/contentful/blocks/find-kinetic/types.ts +20 -20
- package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
- package/src/contentful/blocks/floating-banner/index.test.tsx +246 -246
- package/src/contentful/blocks/floating-banner/index.tsx +97 -97
- package/src/contentful/blocks/floating-banner/types.ts +22 -22
- package/src/contentful/blocks/footer/Footer.stories.tsx +317 -317
- package/src/contentful/blocks/footer/index.test.tsx +302 -302
- package/src/contentful/blocks/footer/index.tsx +91 -91
- package/src/contentful/blocks/footer/types.ts +13 -13
- package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
- package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -61
- package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
- package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -467
- package/src/contentful/blocks/image-promo-bar/index.tsx +8 -6
- package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
- package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -142
- package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
- package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -104
- package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
- package/src/contentful/blocks/modal/constants.ts +53 -53
- package/src/contentful/blocks/modal/index.test.tsx +209 -209
- package/src/contentful/blocks/modal/index.tsx +108 -108
- package/src/contentful/blocks/modal/types.ts +12 -12
- package/src/contentful/blocks/navigation/Navigation.stories.mocks.tsx +78 -78
- package/src/contentful/blocks/navigation/Navigation.stories.tsx +138 -138
- package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -208
- package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +141 -141
- package/src/contentful/blocks/navigation/index.test.tsx +924 -924
- package/src/contentful/blocks/navigation/index.tsx +569 -569
- package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -131
- package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
- package/src/contentful/blocks/navigation/types.ts +71 -71
- package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
- package/src/contentful/blocks/primary-hero/index.test.tsx +286 -286
- package/src/contentful/blocks/primary-hero/index.tsx +239 -239
- package/src/contentful/blocks/primary-hero/types.ts +37 -37
- package/src/contentful/blocks/search-block/index.test.tsx +268 -268
- package/src/contentful/blocks/search-block/index.tsx +90 -90
- package/src/contentful/blocks/search-block/types.ts +15 -15
- package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
- package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -284
- package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
- package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
- package/src/contentful/blocks/text/Text.stories.tsx +23 -23
- package/src/contentful/blocks/text/index.test.tsx +36 -36
- package/src/contentful/blocks/text/index.tsx +12 -12
- package/src/contentful/blocks/text/types.ts +1 -1
- package/src/contentful/index.test.ts +45 -45
- package/src/contentful/index.ts +105 -105
- package/src/global-mocks/contentful/to-document.ts +25 -25
- package/src/global-mocks/cookie.ts +48 -48
- package/src/global-mocks/cx.ts +37 -37
- package/src/global-mocks/index.ts +89 -89
- package/src/global-mocks/speed-card-bg.ts +27 -27
- package/src/global-mocks/utm.ts +49 -49
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -1758
- package/src/hooks/contentful/use-contentful-rich-text.tsx +309 -309
- package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -277
- package/src/hooks/contentful/use-processed-check-list.ts +63 -63
- package/src/hooks/use-body-scroll-lock.test.ts +134 -134
- package/src/hooks/use-body-scroll-lock.ts +34 -34
- package/src/hooks/use-carousel-swipe.test.ts +393 -393
- package/src/hooks/use-carousel-swipe.ts +264 -264
- package/src/hooks/use-outside-click.test.ts +142 -142
- package/src/hooks/use-outside-click.ts +17 -17
- package/src/index.ts +107 -107
- package/src/next/index.test.ts +7 -7
- package/src/next/index.ts +5 -5
- package/src/setupTests.ts +52 -52
- package/src/stories/DocsTemplate.tsx +24 -24
- package/src/styles/globals.css +343 -343
- package/src/types/global.d.ts +9 -9
- package/src/types/micro-components.ts +99 -99
- package/src/types/utm.ts +49 -49
- package/src/utils/contentful/to-document.test.ts +85 -85
- package/src/utils/contentful/to-document.ts +24 -24
- package/src/utils/cookie.test.ts +180 -180
- package/src/utils/cookie.ts +84 -84
- package/src/utils/cx.test.ts +90 -90
- package/src/utils/cx.ts +49 -49
- package/src/utils/index.test.ts +115 -115
- package/src/utils/index.ts +41 -41
- package/src/utils/speed-card-bg.test.ts +46 -46
- package/src/utils/speed-card-bg.ts +24 -24
- package/src/utils/utm.test.ts +359 -359
- package/src/utils/utm.ts +221 -221
|
@@ -1,924 +1,924 @@
|
|
|
1
|
-
import "@testing-library/jest-dom";
|
|
2
|
-
|
|
3
|
-
import React from "react";
|
|
4
|
-
import { Navigation } from "./index";
|
|
5
|
-
import { NavigationProps } from "./types";
|
|
6
|
-
|
|
7
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
8
|
-
|
|
9
|
-
jest.mock("./desktop-link-groups.tsx", () => ({
|
|
10
|
-
DesktopLinkGroups: ({ anchorName, link }: any) => (
|
|
11
|
-
<div data-testid={`desktop-link-group-${anchorName}`}>
|
|
12
|
-
{link?.title || link?.buttonLabel}
|
|
13
|
-
</div>
|
|
14
|
-
),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
jest.mock("./mobile-link-groups.tsx", () => ({
|
|
18
|
-
MobileLinkGroups: ({ link }: any) => (
|
|
19
|
-
<div data-testid="mobile-link-group">
|
|
20
|
-
{link?.title || link?.buttonLabel}
|
|
21
|
-
</div>
|
|
22
|
-
),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
jest.mock("@shared/components/call-button", () => ({
|
|
26
|
-
CallButton: ({ children, href, onClick, className }: any) => (
|
|
27
|
-
<a
|
|
28
|
-
data-testid="call-button"
|
|
29
|
-
href={href}
|
|
30
|
-
className={className}
|
|
31
|
-
onClick={onClick}
|
|
32
|
-
>
|
|
33
|
-
{children}
|
|
34
|
-
</a>
|
|
35
|
-
),
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
jest.mock("@shared/components/input", () => ({
|
|
39
|
-
// eslint-disable-next-line react/display-name
|
|
40
|
-
Input: React.forwardRef(
|
|
41
|
-
(
|
|
42
|
-
{
|
|
43
|
-
name,
|
|
44
|
-
value,
|
|
45
|
-
onChange,
|
|
46
|
-
placeholder,
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
48
|
-
className,
|
|
49
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
50
|
-
containerClassName,
|
|
51
|
-
}: any,
|
|
52
|
-
ref: any
|
|
53
|
-
) => (
|
|
54
|
-
<input
|
|
55
|
-
data-testid={`input-${name}`}
|
|
56
|
-
ref={ref}
|
|
57
|
-
name={name}
|
|
58
|
-
value={value}
|
|
59
|
-
onChange={onChange}
|
|
60
|
-
placeholder={placeholder}
|
|
61
|
-
/>
|
|
62
|
-
)
|
|
63
|
-
),
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
|
-
jest.mock("@shared/components/link", () => ({
|
|
67
|
-
Link: ({ children, href, className, onClick, ...rest }: any) => (
|
|
68
|
-
<a
|
|
69
|
-
data-testid="link"
|
|
70
|
-
href={href}
|
|
71
|
-
className={className}
|
|
72
|
-
onClick={onClick}
|
|
73
|
-
{...rest}
|
|
74
|
-
>
|
|
75
|
-
{children}
|
|
76
|
-
</a>
|
|
77
|
-
),
|
|
78
|
-
}));
|
|
79
|
-
|
|
80
|
-
jest.mock("@shared/components/material-icon", () => ({
|
|
81
|
-
MaterialIcon: ({ name }: any) => (
|
|
82
|
-
<span data-testid={`icon-${name}`}>{name}</span>
|
|
83
|
-
),
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
jest.mock("@shared/components/next-image", () => ({
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
88
|
-
NextImage: ({ src, alt, onClick, ...rest }: any) => (
|
|
89
|
-
<img data-testid="next-image" src={src} alt={alt} onClick={onClick} />
|
|
90
|
-
),
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
|
-
jest.mock("@shared/components/text", () => ({
|
|
94
|
-
Text: ({ as: Tag = "span", children, className }: any) => (
|
|
95
|
-
<Tag className={className}>{children}</Tag>
|
|
96
|
-
),
|
|
97
|
-
}));
|
|
98
|
-
|
|
99
|
-
jest.mock("@shared/contentful/blocks/button", () => ({
|
|
100
|
-
Button: ({
|
|
101
|
-
children,
|
|
102
|
-
buttonClassName,
|
|
103
|
-
onClick,
|
|
104
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
105
|
-
showButtonAs,
|
|
106
|
-
linkClassName,
|
|
107
|
-
...rest
|
|
108
|
-
}: any) => (
|
|
109
|
-
<button
|
|
110
|
-
data-testid="contentful-button"
|
|
111
|
-
className={buttonClassName || linkClassName}
|
|
112
|
-
onClick={onClick}
|
|
113
|
-
>
|
|
114
|
-
{children || rest.buttonLabel}
|
|
115
|
-
</button>
|
|
116
|
-
),
|
|
117
|
-
__esModule: true,
|
|
118
|
-
}));
|
|
119
|
-
|
|
120
|
-
jest.mock("@shared/utils", () => ({
|
|
121
|
-
cx: (...args: any[]) => args.filter(Boolean).join(" "),
|
|
122
|
-
}));
|
|
123
|
-
|
|
124
|
-
const defaultProps: NavigationProps = {
|
|
125
|
-
primaryNavigationLinks: [],
|
|
126
|
-
utilityNavigationLinks: [],
|
|
127
|
-
primaryNavigationLogo: "https://logo.test/logo.png",
|
|
128
|
-
accountNavigationLinks: [],
|
|
129
|
-
supportNavigationLinks: [],
|
|
130
|
-
searchBarIcon: "https://icon.test/search.png",
|
|
131
|
-
invocaPhoneNumberLink: "tel:1234567890",
|
|
132
|
-
invocaPhoneNumberDisplayText: "123-456-7890",
|
|
133
|
-
callNowCtaText: "Call Now",
|
|
134
|
-
displayCartIcon: false,
|
|
135
|
-
cartHref: "/cart",
|
|
136
|
-
cartHasRetention: false,
|
|
137
|
-
cartIconAriaLabel: "Cart",
|
|
138
|
-
onSearch: jest.fn(),
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
// Props that include primaryNavigationLinks so MobileMenu renders
|
|
142
|
-
const mobileMenuProps: NavigationProps = {
|
|
143
|
-
...defaultProps,
|
|
144
|
-
primaryNavigationLinks: [
|
|
145
|
-
{ buttonLabel: "Products", href: "/products" },
|
|
146
|
-
] as any,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
describe("Navigation", () => {
|
|
150
|
-
beforeEach(() => jest.clearAllMocks());
|
|
151
|
-
|
|
152
|
-
describe("Basic rendering", () => {
|
|
153
|
-
it("renders nav with utility and main nav sections", () => {
|
|
154
|
-
const { container } = render(<Navigation {...defaultProps} />);
|
|
155
|
-
expect(container.querySelector(".menu-container")).toBeInTheDocument();
|
|
156
|
-
expect(container.querySelector(".utility-container")).toBeInTheDocument();
|
|
157
|
-
expect(
|
|
158
|
-
container.querySelector(".desktop-nav-section")
|
|
159
|
-
).toBeInTheDocument();
|
|
160
|
-
expect(
|
|
161
|
-
container.querySelector(".mobile-nav-section")
|
|
162
|
-
).toBeInTheDocument();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("renders logo with string src", () => {
|
|
166
|
-
render(<Navigation {...defaultProps} />);
|
|
167
|
-
const logos = screen.getAllByAltText("Kinetic business logo");
|
|
168
|
-
expect(logos[0]).toHaveAttribute("src", "https://logo.test/logo.png");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("renders logo with object src (Asset)", () => {
|
|
172
|
-
render(
|
|
173
|
-
<Navigation
|
|
174
|
-
{...defaultProps}
|
|
175
|
-
primaryNavigationLogo={{ url: "https://logo.test/obj.png" } as any}
|
|
176
|
-
/>
|
|
177
|
-
);
|
|
178
|
-
const logos = screen.getAllByAltText("Kinetic business logo");
|
|
179
|
-
expect(logos[0]).toHaveAttribute("src", "https://logo.test/obj.png");
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("does not render logo when logo url is empty", () => {
|
|
183
|
-
const { container } = render(
|
|
184
|
-
<Navigation {...defaultProps} primaryNavigationLogo={{} as any} />
|
|
185
|
-
);
|
|
186
|
-
// logoUrl is "" so the conditional renders null
|
|
187
|
-
expect(
|
|
188
|
-
container.querySelector("[alt='Kinetic business logo']")
|
|
189
|
-
).not.toBeInTheDocument();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("renders phone number display text", () => {
|
|
193
|
-
render(<Navigation {...defaultProps} />);
|
|
194
|
-
expect(screen.getAllByText("123-456-7890").length).toBeGreaterThan(0);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe("Utility navigation links", () => {
|
|
199
|
-
it("renders utility nav links", () => {
|
|
200
|
-
const links = [
|
|
201
|
-
{ buttonLabel: "Residential", href: "/residential" },
|
|
202
|
-
{ buttonLabel: "Business", href: "/business" },
|
|
203
|
-
];
|
|
204
|
-
render(
|
|
205
|
-
<Navigation {...defaultProps} utilityNavigationLinks={links as any} />
|
|
206
|
-
);
|
|
207
|
-
const buttons = screen.getAllByTestId("contentful-button");
|
|
208
|
-
expect(buttons.length).toBeGreaterThanOrEqual(2);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("applies active class to second link by default (no utilityNavActiveIndex)", () => {
|
|
212
|
-
const links = [
|
|
213
|
-
{ buttonLabel: "Residential", href: "/residential" },
|
|
214
|
-
{ buttonLabel: "Business", href: "/business" },
|
|
215
|
-
];
|
|
216
|
-
const { container } = render(
|
|
217
|
-
<Navigation {...defaultProps} utilityNavigationLinks={links as any} />
|
|
218
|
-
);
|
|
219
|
-
const utilityButtons = container.querySelectorAll(
|
|
220
|
-
".utility-container button"
|
|
221
|
-
);
|
|
222
|
-
expect(utilityButtons[1]?.className).toContain("label4");
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("applies active class based on utilityNavActiveIndex", () => {
|
|
226
|
-
const links = [
|
|
227
|
-
{ buttonLabel: "Residential", href: "/residential" },
|
|
228
|
-
{ buttonLabel: "Business", href: "/business" },
|
|
229
|
-
];
|
|
230
|
-
const { container } = render(
|
|
231
|
-
<Navigation
|
|
232
|
-
{...defaultProps}
|
|
233
|
-
utilityNavigationLinks={links as any}
|
|
234
|
-
utilityNavActiveIndex={0}
|
|
235
|
-
/>
|
|
236
|
-
);
|
|
237
|
-
const utilityButtons = container.querySelectorAll(
|
|
238
|
-
".utility-container button"
|
|
239
|
-
);
|
|
240
|
-
expect(utilityButtons[0]?.className).toContain("label4");
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
describe("Cart icon", () => {
|
|
245
|
-
it("renders cart icon when displayCartIcon is true", () => {
|
|
246
|
-
render(<Navigation {...defaultProps} displayCartIcon={true} />);
|
|
247
|
-
expect(
|
|
248
|
-
screen.getAllByTestId("icon-shopping_cart").length
|
|
249
|
-
).toBeGreaterThan(0);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("shows retention dot when cartHasRetention is true", () => {
|
|
253
|
-
const { container } = render(
|
|
254
|
-
<Navigation
|
|
255
|
-
{...defaultProps}
|
|
256
|
-
displayCartIcon={true}
|
|
257
|
-
cartHasRetention={true}
|
|
258
|
-
/>
|
|
259
|
-
);
|
|
260
|
-
expect(container.querySelector(".bg-icon-brand")).toBeInTheDocument();
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("does not render cart icon when displayCartIcon is false", () => {
|
|
264
|
-
render(<Navigation {...defaultProps} displayCartIcon={false} />);
|
|
265
|
-
expect(
|
|
266
|
-
screen.queryByTestId("icon-shopping_cart")
|
|
267
|
-
).not.toBeInTheDocument();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it("calls onCartClick when cart is clicked", () => {
|
|
271
|
-
const onCartClick = jest.fn();
|
|
272
|
-
render(
|
|
273
|
-
<Navigation
|
|
274
|
-
{...defaultProps}
|
|
275
|
-
displayCartIcon={true}
|
|
276
|
-
onCartClick={onCartClick}
|
|
277
|
-
/>
|
|
278
|
-
);
|
|
279
|
-
const cartLinks = screen.getAllByLabelText("Cart");
|
|
280
|
-
fireEvent.click(cartLinks[0]);
|
|
281
|
-
expect(onCartClick).toHaveBeenCalled();
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
describe("Primary navigation links", () => {
|
|
286
|
-
it("renders DesktopLinkGroups for primary nav links", () => {
|
|
287
|
-
const links = [{ title: "Products", items: { items: [] } }];
|
|
288
|
-
render(
|
|
289
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
290
|
-
);
|
|
291
|
-
expect(
|
|
292
|
-
screen.getByTestId("desktop-link-group-main-menu-0")
|
|
293
|
-
).toBeInTheDocument();
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
describe("Account navigation links", () => {
|
|
298
|
-
it("renders DesktopLinkGroups for account links", () => {
|
|
299
|
-
const links = [{ title: "My Account", items: { items: [] } }];
|
|
300
|
-
render(
|
|
301
|
-
<Navigation {...defaultProps} accountNavigationLinks={links as any} />
|
|
302
|
-
);
|
|
303
|
-
expect(
|
|
304
|
-
screen.getByTestId("desktop-link-group-my-account-0")
|
|
305
|
-
).toBeInTheDocument();
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
describe("Support navigation links", () => {
|
|
310
|
-
it("renders DesktopLinkGroups for support links", () => {
|
|
311
|
-
const links = [{ title: "Support", items: { items: [] } }];
|
|
312
|
-
render(
|
|
313
|
-
<Navigation {...defaultProps} supportNavigationLinks={links as any} />
|
|
314
|
-
);
|
|
315
|
-
expect(
|
|
316
|
-
screen.getByTestId("desktop-link-group-support-menu-0")
|
|
317
|
-
).toBeInTheDocument();
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
describe("Search", () => {
|
|
322
|
-
it("renders desktop search input", () => {
|
|
323
|
-
render(<Navigation {...defaultProps} />);
|
|
324
|
-
expect(
|
|
325
|
-
screen.getAllByPlaceholderText("Search...").length
|
|
326
|
-
).toBeGreaterThan(0);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it("calls onSearch when desktop search form is submitted", () => {
|
|
330
|
-
const onSearch = jest.fn();
|
|
331
|
-
const { container } = render(
|
|
332
|
-
<Navigation {...defaultProps} onSearch={onSearch} />
|
|
333
|
-
);
|
|
334
|
-
const desktopForm = container.querySelector(".desktop-nav-section form");
|
|
335
|
-
const input = desktopForm?.querySelector("input");
|
|
336
|
-
fireEvent.change(input!, { target: { value: "test query" } });
|
|
337
|
-
fireEvent.submit(desktopForm!);
|
|
338
|
-
expect(onSearch).toHaveBeenCalledWith("test query");
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it("calls onSearch when search icon is clicked", () => {
|
|
342
|
-
const onSearch = jest.fn();
|
|
343
|
-
const { container } = render(
|
|
344
|
-
<Navigation
|
|
345
|
-
{...defaultProps}
|
|
346
|
-
onSearch={onSearch}
|
|
347
|
-
searchBarIcon="https://icon.test/s.png"
|
|
348
|
-
/>
|
|
349
|
-
);
|
|
350
|
-
const desktopForm = container.querySelector(".desktop-nav-section form");
|
|
351
|
-
const input = desktopForm?.querySelector("input");
|
|
352
|
-
fireEvent.change(input!, { target: { value: "click search" } });
|
|
353
|
-
const searchIcon = desktopForm?.querySelector("img");
|
|
354
|
-
fireEvent.click(searchIcon!);
|
|
355
|
-
expect(onSearch).toHaveBeenCalledWith("click search");
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it("uses searchBarIcon url from object", () => {
|
|
359
|
-
render(
|
|
360
|
-
<Navigation
|
|
361
|
-
{...defaultProps}
|
|
362
|
-
searchBarIcon={{ url: "https://icon.test/obj.png" } as any}
|
|
363
|
-
/>
|
|
364
|
-
);
|
|
365
|
-
const imgs = screen.getAllByAltText("Search icon");
|
|
366
|
-
expect(imgs[0]).toHaveAttribute("src", "https://icon.test/obj.png");
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
describe("Mobile menu", () => {
|
|
371
|
-
it("does not render mobile menu when no nav links provided", () => {
|
|
372
|
-
const { container } = render(<Navigation {...defaultProps} />);
|
|
373
|
-
const menuButton = container.querySelector(
|
|
374
|
-
".mobile-nav-section [data-testid='icon-menu']"
|
|
375
|
-
);
|
|
376
|
-
expect(menuButton).not.toBeInTheDocument();
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it("opens mobile menu on hamburger click", () => {
|
|
380
|
-
const { container } = render(<Navigation {...mobileMenuProps} />);
|
|
381
|
-
const menuButton = container
|
|
382
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
383
|
-
?.closest("button");
|
|
384
|
-
fireEvent.click(menuButton!);
|
|
385
|
-
expect(
|
|
386
|
-
container.querySelector("#mobile-menu-overlay")
|
|
387
|
-
).toBeInTheDocument();
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it("closes mobile menu on close button click", () => {
|
|
391
|
-
const { container } = render(<Navigation {...mobileMenuProps} />);
|
|
392
|
-
const menuButton = container
|
|
393
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
394
|
-
?.closest("button");
|
|
395
|
-
fireEvent.click(menuButton!);
|
|
396
|
-
const closeButton = container
|
|
397
|
-
.querySelector("#drawer-items [data-testid='icon-close']")
|
|
398
|
-
?.closest("button");
|
|
399
|
-
fireEvent.click(closeButton!);
|
|
400
|
-
expect(
|
|
401
|
-
container.querySelector("#mobile-menu-overlay")?.className
|
|
402
|
-
).toContain("-right-96");
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("renders mobile search input and submits", () => {
|
|
406
|
-
const onSearch = jest.fn();
|
|
407
|
-
const { container } = render(
|
|
408
|
-
<Navigation {...mobileMenuProps} onSearch={onSearch} />
|
|
409
|
-
);
|
|
410
|
-
const menuButton = container
|
|
411
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
412
|
-
?.closest("button");
|
|
413
|
-
fireEvent.click(menuButton!);
|
|
414
|
-
const mobileForm = container.querySelector("#drawer-items form");
|
|
415
|
-
const input = mobileForm?.querySelector("input");
|
|
416
|
-
fireEvent.change(input!, { target: { value: "mobile search" } });
|
|
417
|
-
fireEvent.submit(mobileForm!);
|
|
418
|
-
expect(onSearch).toHaveBeenCalledWith("mobile search");
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
it("closes menu when a link is clicked in the drawer", () => {
|
|
422
|
-
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
423
|
-
const { container } = render(
|
|
424
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
425
|
-
);
|
|
426
|
-
const menuButton = container
|
|
427
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
428
|
-
?.closest("button");
|
|
429
|
-
fireEvent.click(menuButton!);
|
|
430
|
-
expect(
|
|
431
|
-
container.querySelector("#mobile-menu-overlay")?.className
|
|
432
|
-
).toContain("right-0");
|
|
433
|
-
expect(
|
|
434
|
-
container.querySelector("#mobile-menu-overlay")?.className
|
|
435
|
-
).not.toContain("-right-96");
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it("renders MobileLinkGroups in drawer", () => {
|
|
439
|
-
const links = [{ title: "Products", items: { items: [] } }];
|
|
440
|
-
const { container } = render(
|
|
441
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
442
|
-
);
|
|
443
|
-
const menuButton = container
|
|
444
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
445
|
-
?.closest("button");
|
|
446
|
-
fireEvent.click(menuButton!);
|
|
447
|
-
expect(screen.getAllByTestId("mobile-link-group").length).toBeGreaterThan(
|
|
448
|
-
0
|
|
449
|
-
);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it("closes menu when an anchor inside the drawer content is clicked", () => {
|
|
453
|
-
const links = [{ buttonLabel: "Internet", href: "/internet" }];
|
|
454
|
-
const { container } = render(
|
|
455
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
456
|
-
);
|
|
457
|
-
const menuButton = container
|
|
458
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
459
|
-
?.closest("button");
|
|
460
|
-
fireEvent.click(menuButton!);
|
|
461
|
-
expect(
|
|
462
|
-
container.querySelector("#mobile-menu-overlay")?.className
|
|
463
|
-
).toContain("right-0");
|
|
464
|
-
const scrollDiv = container.querySelector(
|
|
465
|
-
"#drawer-items .flex-grow.overflow-y-auto"
|
|
466
|
-
);
|
|
467
|
-
const anchor = document.createElement("a");
|
|
468
|
-
anchor.href = "/internet";
|
|
469
|
-
scrollDiv!.appendChild(anchor);
|
|
470
|
-
fireEvent.click(anchor);
|
|
471
|
-
expect(
|
|
472
|
-
container.querySelector("#mobile-menu-overlay")?.className
|
|
473
|
-
).toContain("-right-96");
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it("does not close menu when non-anchor element is clicked in drawer", () => {
|
|
477
|
-
const links = [{ title: "Products", items: { items: [] } }];
|
|
478
|
-
const { container } = render(
|
|
479
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
480
|
-
);
|
|
481
|
-
const menuButton = container
|
|
482
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
483
|
-
?.closest("button");
|
|
484
|
-
fireEvent.click(menuButton!);
|
|
485
|
-
const scrollDiv = container.querySelector(
|
|
486
|
-
"#drawer-items .flex-grow.overflow-y-auto"
|
|
487
|
-
);
|
|
488
|
-
fireEvent.click(scrollDiv!);
|
|
489
|
-
expect(
|
|
490
|
-
container.querySelector("#mobile-menu-overlay")?.className
|
|
491
|
-
).toContain("right-0");
|
|
492
|
-
expect(
|
|
493
|
-
container.querySelector("#mobile-menu-overlay")?.className
|
|
494
|
-
).not.toContain("-right-96");
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
it("traps focus with Tab key - wraps from last to first", () => {
|
|
498
|
-
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
499
|
-
const { container } = render(
|
|
500
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
501
|
-
);
|
|
502
|
-
const menuButton = container
|
|
503
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
504
|
-
?.closest("button");
|
|
505
|
-
fireEvent.click(menuButton!);
|
|
506
|
-
|
|
507
|
-
const drawerItems = container.querySelector("#drawer-items");
|
|
508
|
-
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
509
|
-
const firstEl = focusableEls[0] as HTMLElement;
|
|
510
|
-
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
|
|
511
|
-
|
|
512
|
-
lastEl.focus();
|
|
513
|
-
expect(document.activeElement).toBe(lastEl);
|
|
514
|
-
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: false });
|
|
515
|
-
expect(document.activeElement).toBe(firstEl);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it("traps focus with Shift+Tab - wraps from first to last", () => {
|
|
519
|
-
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
520
|
-
const { container } = render(
|
|
521
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
522
|
-
);
|
|
523
|
-
const menuButton = container
|
|
524
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
525
|
-
?.closest("button");
|
|
526
|
-
fireEvent.click(menuButton!);
|
|
527
|
-
|
|
528
|
-
const drawerItems = container.querySelector("#drawer-items");
|
|
529
|
-
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
530
|
-
const firstEl = focusableEls[0] as HTMLElement;
|
|
531
|
-
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
|
|
532
|
-
|
|
533
|
-
firstEl.focus();
|
|
534
|
-
expect(document.activeElement).toBe(firstEl);
|
|
535
|
-
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: true });
|
|
536
|
-
expect(document.activeElement).toBe(lastEl);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
it("does not trap focus for non-Tab keys", () => {
|
|
540
|
-
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
541
|
-
const { container } = render(
|
|
542
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
543
|
-
);
|
|
544
|
-
const menuButton = container
|
|
545
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
546
|
-
?.closest("button");
|
|
547
|
-
fireEvent.click(menuButton!);
|
|
548
|
-
|
|
549
|
-
const drawerItems = container.querySelector("#drawer-items");
|
|
550
|
-
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
551
|
-
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
|
|
552
|
-
lastEl.focus();
|
|
553
|
-
|
|
554
|
-
fireEvent.keyDown(window, { key: "Enter", keyCode: 13 });
|
|
555
|
-
expect(document.activeElement).toBe(lastEl);
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
it("hides mobile call button when hideMobileCallButton is true", () => {
|
|
559
|
-
const { container } = render(
|
|
560
|
-
<Navigation {...mobileMenuProps} hideMobileCallButton={true} />
|
|
561
|
-
);
|
|
562
|
-
const mobileGapDiv = container.querySelector(
|
|
563
|
-
".mobile-nav-section .flex.items-center.gap-6"
|
|
564
|
-
);
|
|
565
|
-
const directCallButtons = mobileGapDiv?.querySelectorAll(
|
|
566
|
-
":scope > [data-testid='call-button']"
|
|
567
|
-
);
|
|
568
|
-
expect(directCallButtons?.length || 0).toBe(0);
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
it("shows mobile call button by default", () => {
|
|
572
|
-
const { container } = render(<Navigation {...mobileMenuProps} />);
|
|
573
|
-
const mobileSection = container.querySelector(".mobile-nav-section");
|
|
574
|
-
const callButton = mobileSection?.querySelector(
|
|
575
|
-
"[data-testid='call-button']"
|
|
576
|
-
);
|
|
577
|
-
expect(callButton).toBeInTheDocument();
|
|
578
|
-
});
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
describe("checkPlansJSX", () => {
|
|
582
|
-
it("renders checkPlansJSX when provided", () => {
|
|
583
|
-
render(
|
|
584
|
-
<Navigation
|
|
585
|
-
{...defaultProps}
|
|
586
|
-
checkPlansJSX={<div data-testid="check-plans">Plans</div>}
|
|
587
|
-
/>
|
|
588
|
-
);
|
|
589
|
-
expect(screen.getByTestId("check-plans")).toBeInTheDocument();
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
it("does not render checkPlansJSX when not provided", () => {
|
|
593
|
-
render(<Navigation {...defaultProps} />);
|
|
594
|
-
expect(screen.queryByTestId("check-plans")).not.toBeInTheDocument();
|
|
595
|
-
});
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
describe("Logo dimensions", () => {
|
|
599
|
-
it("uses custom logo dimensions", () => {
|
|
600
|
-
render(
|
|
601
|
-
<Navigation
|
|
602
|
-
{...defaultProps}
|
|
603
|
-
primaryNavigationLogoWidth={100}
|
|
604
|
-
primaryNavigationLogoHeight={32}
|
|
605
|
-
/>
|
|
606
|
-
);
|
|
607
|
-
const logos = screen.getAllByAltText("Kinetic business logo");
|
|
608
|
-
expect(logos.length).toBeGreaterThan(0);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
it("renders logo from object src in mobile section", () => {
|
|
612
|
-
render(
|
|
613
|
-
<Navigation
|
|
614
|
-
{...defaultProps}
|
|
615
|
-
primaryNavigationLogo={{ url: "https://logo.test/m.png" } as any}
|
|
616
|
-
/>
|
|
617
|
-
);
|
|
618
|
-
const logos = screen.getAllByAltText("Kinetic business logo");
|
|
619
|
-
expect(logos[0]).toHaveAttribute("src", "https://logo.test/m.png");
|
|
620
|
-
});
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
describe("Null/undefined optional arrays (branch coverage)", () => {
|
|
624
|
-
it("renders without crashing when utilityNavigationLinks is undefined", () => {
|
|
625
|
-
const { container } = render(
|
|
626
|
-
<Navigation
|
|
627
|
-
{...defaultProps}
|
|
628
|
-
utilityNavigationLinks={undefined as any}
|
|
629
|
-
/>
|
|
630
|
-
);
|
|
631
|
-
expect(container.querySelector(".menu-container")).toBeInTheDocument();
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
it("renders without crashing when primaryNavigationLinks is undefined", () => {
|
|
635
|
-
const { container } = render(
|
|
636
|
-
<Navigation
|
|
637
|
-
{...defaultProps}
|
|
638
|
-
primaryNavigationLinks={undefined as any}
|
|
639
|
-
/>
|
|
640
|
-
);
|
|
641
|
-
expect(
|
|
642
|
-
container.querySelector(".desktop-nav-section")
|
|
643
|
-
).toBeInTheDocument();
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
it("renders without crashing when accountNavigationLinks is undefined", () => {
|
|
647
|
-
const { container } = render(
|
|
648
|
-
<Navigation
|
|
649
|
-
{...defaultProps}
|
|
650
|
-
accountNavigationLinks={undefined as any}
|
|
651
|
-
/>
|
|
652
|
-
);
|
|
653
|
-
expect(container.querySelector(".menu-container")).toBeInTheDocument();
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
it("renders without crashing when supportNavigationLinks is undefined", () => {
|
|
657
|
-
const { container } = render(
|
|
658
|
-
<Navigation
|
|
659
|
-
{...defaultProps}
|
|
660
|
-
supportNavigationLinks={undefined as any}
|
|
661
|
-
/>
|
|
662
|
-
);
|
|
663
|
-
expect(container.querySelector(".menu-container")).toBeInTheDocument();
|
|
664
|
-
});
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
describe("Mobile cart icon", () => {
|
|
668
|
-
it("renders cart icon in mobile section when displayCartIcon is true", () => {
|
|
669
|
-
const { container } = render(
|
|
670
|
-
<Navigation {...mobileMenuProps} displayCartIcon={true} />
|
|
671
|
-
);
|
|
672
|
-
const mobileSection = container.querySelector(".mobile-nav-section");
|
|
673
|
-
expect(
|
|
674
|
-
mobileSection?.querySelector("[data-testid='icon-shopping_cart']")
|
|
675
|
-
).toBeInTheDocument();
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
it("shows retention dot on mobile cart when cartHasRetention is true", () => {
|
|
679
|
-
const { container } = render(
|
|
680
|
-
<Navigation
|
|
681
|
-
{...mobileMenuProps}
|
|
682
|
-
displayCartIcon={true}
|
|
683
|
-
cartHasRetention={true}
|
|
684
|
-
/>
|
|
685
|
-
);
|
|
686
|
-
const mobileSection = container.querySelector(".mobile-nav-section");
|
|
687
|
-
expect(
|
|
688
|
-
mobileSection?.querySelector(".bg-icon-brand")
|
|
689
|
-
).toBeInTheDocument();
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
it("does not show retention dot on mobile when cartHasRetention is false", () => {
|
|
693
|
-
const { container } = render(
|
|
694
|
-
<Navigation
|
|
695
|
-
{...mobileMenuProps}
|
|
696
|
-
displayCartIcon={true}
|
|
697
|
-
cartHasRetention={false}
|
|
698
|
-
/>
|
|
699
|
-
);
|
|
700
|
-
const mobileSection = container.querySelector(".mobile-nav-section");
|
|
701
|
-
const dots = mobileSection?.querySelectorAll(".bg-icon-brand");
|
|
702
|
-
expect(dots?.length || 0).toBe(0);
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
it("uses fallback '#' when cartHref is undefined", () => {
|
|
706
|
-
const { container } = render(
|
|
707
|
-
<Navigation
|
|
708
|
-
{...mobileMenuProps}
|
|
709
|
-
displayCartIcon={true}
|
|
710
|
-
cartHref={undefined as any}
|
|
711
|
-
/>
|
|
712
|
-
);
|
|
713
|
-
const cartLinks = container.querySelectorAll("[aria-label='Cart']");
|
|
714
|
-
cartLinks.forEach(link => {
|
|
715
|
-
expect(link).toHaveAttribute("href", "#");
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
describe("Mobile drawer utility links", () => {
|
|
721
|
-
it("renders utility links in mobile drawer", () => {
|
|
722
|
-
const links = [
|
|
723
|
-
{ buttonLabel: "Residential", href: "/residential", anchorId: "r1" },
|
|
724
|
-
{ buttonLabel: "Business", href: "/business", anchorId: "b1" },
|
|
725
|
-
];
|
|
726
|
-
const { container } = render(
|
|
727
|
-
<Navigation
|
|
728
|
-
{...mobileMenuProps}
|
|
729
|
-
utilityNavigationLinks={links as any}
|
|
730
|
-
/>
|
|
731
|
-
);
|
|
732
|
-
const menuButton = container
|
|
733
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
734
|
-
?.closest("button");
|
|
735
|
-
fireEvent.click(menuButton!);
|
|
736
|
-
const drawerButtons = container.querySelectorAll(
|
|
737
|
-
"#drawer-items .bg-bg-fill-info button"
|
|
738
|
-
);
|
|
739
|
-
expect(drawerButtons.length).toBe(2);
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
it("applies active class to utility links in drawer using utilityNavActiveIndex", () => {
|
|
743
|
-
const links = [
|
|
744
|
-
{ buttonLabel: "Residential", href: "/residential", anchorId: "r1" },
|
|
745
|
-
{ buttonLabel: "Business", href: "/business", anchorId: "b1" },
|
|
746
|
-
];
|
|
747
|
-
const { container } = render(
|
|
748
|
-
<Navigation
|
|
749
|
-
{...mobileMenuProps}
|
|
750
|
-
utilityNavigationLinks={links as any}
|
|
751
|
-
utilityNavActiveIndex={0}
|
|
752
|
-
/>
|
|
753
|
-
);
|
|
754
|
-
const menuButton = container
|
|
755
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
756
|
-
?.closest("button");
|
|
757
|
-
fireEvent.click(menuButton!);
|
|
758
|
-
const drawerButtons = container.querySelectorAll(
|
|
759
|
-
"#drawer-items .bg-bg-fill-info button"
|
|
760
|
-
);
|
|
761
|
-
expect(drawerButtons[0]?.className).toContain("label4");
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
it("handles undefined utilityNavigationLinks in mobile drawer", () => {
|
|
765
|
-
const { container } = render(
|
|
766
|
-
<Navigation
|
|
767
|
-
{...mobileMenuProps}
|
|
768
|
-
utilityNavigationLinks={undefined as any}
|
|
769
|
-
/>
|
|
770
|
-
);
|
|
771
|
-
const menuButton = container
|
|
772
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
773
|
-
?.closest("button");
|
|
774
|
-
fireEvent.click(menuButton!);
|
|
775
|
-
const drawerButtons = container.querySelectorAll(
|
|
776
|
-
"#drawer-items .bg-bg-fill-info button"
|
|
777
|
-
);
|
|
778
|
-
expect(drawerButtons.length).toBe(0);
|
|
779
|
-
});
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
describe("Mobile search with object searchBarIcon", () => {
|
|
783
|
-
it("uses searchBarIcon url from object in mobile menu", () => {
|
|
784
|
-
const { container } = render(
|
|
785
|
-
<Navigation
|
|
786
|
-
{...mobileMenuProps}
|
|
787
|
-
searchBarIcon={{ url: "https://icon.test/mobile.png" } as any}
|
|
788
|
-
/>
|
|
789
|
-
);
|
|
790
|
-
const menuButton = container
|
|
791
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
792
|
-
?.closest("button");
|
|
793
|
-
fireEvent.click(menuButton!);
|
|
794
|
-
const mobileForm = container.querySelector("#drawer-items form");
|
|
795
|
-
const img = mobileForm?.querySelector("img");
|
|
796
|
-
expect(img).toHaveAttribute("src", "https://icon.test/mobile.png");
|
|
797
|
-
});
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
describe("Focus trap edge cases", () => {
|
|
801
|
-
it("does not wrap focus when Tab pressed and not on last element", () => {
|
|
802
|
-
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
803
|
-
const { container } = render(
|
|
804
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
805
|
-
);
|
|
806
|
-
const menuButton = container
|
|
807
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
808
|
-
?.closest("button");
|
|
809
|
-
fireEvent.click(menuButton!);
|
|
810
|
-
|
|
811
|
-
const drawerItems = container.querySelector("#drawer-items");
|
|
812
|
-
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
813
|
-
const firstEl = focusableEls[0] as HTMLElement;
|
|
814
|
-
|
|
815
|
-
firstEl.focus();
|
|
816
|
-
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: false });
|
|
817
|
-
expect(document.activeElement).toBe(firstEl);
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
it("does not wrap focus when Shift+Tab pressed and not on first element", () => {
|
|
821
|
-
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
822
|
-
const { container } = render(
|
|
823
|
-
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
824
|
-
);
|
|
825
|
-
const menuButton = container
|
|
826
|
-
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
827
|
-
?.closest("button");
|
|
828
|
-
fireEvent.click(menuButton!);
|
|
829
|
-
|
|
830
|
-
const drawerItems = container.querySelector("#drawer-items");
|
|
831
|
-
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
832
|
-
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
|
|
833
|
-
|
|
834
|
-
lastEl.focus();
|
|
835
|
-
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: true });
|
|
836
|
-
expect(document.activeElement).toBe(lastEl);
|
|
837
|
-
});
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
describe("Navigation background color", () => {
|
|
841
|
-
it("applies bg-secondary-navy500 by default", () => {
|
|
842
|
-
const { container } = render(<Navigation {...defaultProps} />);
|
|
843
|
-
const mainNav = container.querySelector(".main-nav-container");
|
|
844
|
-
expect(mainNav?.className).toContain("bg-secondary-navy500");
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
it("applies custom bg class with token format", () => {
|
|
848
|
-
const { container } = render(
|
|
849
|
-
<Navigation {...defaultProps} navigationBackgroundColor="green300" />
|
|
850
|
-
);
|
|
851
|
-
const mainNav = container.querySelector(".main-nav-container");
|
|
852
|
-
expect(mainNav?.className).toContain("bg-secondary-green300");
|
|
853
|
-
});
|
|
854
|
-
|
|
855
|
-
it("passes through raw tailwind class", () => {
|
|
856
|
-
const { container } = render(
|
|
857
|
-
<Navigation {...defaultProps} navigationBackgroundColor="bg-blue-500" />
|
|
858
|
-
);
|
|
859
|
-
const mainNav = container.querySelector(".main-nav-container");
|
|
860
|
-
expect(mainNav?.className).toContain("bg-blue-500");
|
|
861
|
-
});
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
describe("Display options", () => {
|
|
865
|
-
it("hides utility navigation when displayUtilityNavigation is false", () => {
|
|
866
|
-
const { container } = render(
|
|
867
|
-
<Navigation {...defaultProps} displayUtilityNavigation={false} />
|
|
868
|
-
);
|
|
869
|
-
expect(
|
|
870
|
-
container.querySelector(".utility-container")
|
|
871
|
-
).not.toBeInTheDocument();
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
it("hides search bar when displaySearchBar is false", () => {
|
|
875
|
-
const { container } = render(
|
|
876
|
-
<Navigation {...defaultProps} displaySearchBar={false} />
|
|
877
|
-
);
|
|
878
|
-
const desktopForm = container.querySelector(".desktop-nav-section form");
|
|
879
|
-
expect(desktopForm).not.toBeInTheDocument();
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
it("hides call now CTA when displayCallNowCta is false", () => {
|
|
883
|
-
const { container } = render(
|
|
884
|
-
<Navigation {...defaultProps} displayCallNowCta={false} />
|
|
885
|
-
);
|
|
886
|
-
const utilityCta = container.querySelector(
|
|
887
|
-
".utility-container [data-testid='call-button']"
|
|
888
|
-
);
|
|
889
|
-
expect(utilityCta).not.toBeInTheDocument();
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
it("renders main nav call CTA when showCallNowCtaInMainNav is true", () => {
|
|
893
|
-
render(
|
|
894
|
-
<Navigation
|
|
895
|
-
{...defaultProps}
|
|
896
|
-
showCallNowCtaInMainNav={true}
|
|
897
|
-
displayUtilityNavigation={false}
|
|
898
|
-
invocaPhoneNumberLink="tel:1234567890"
|
|
899
|
-
invocaPhoneNumberDisplayText="123-456-7890"
|
|
900
|
-
/>
|
|
901
|
-
);
|
|
902
|
-
const ctaLink = screen
|
|
903
|
-
.getAllByTestId("link")
|
|
904
|
-
.find(el => el.getAttribute("aria-label") === "Call 123-456-7890");
|
|
905
|
-
expect(ctaLink).toBeInTheDocument();
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
it("renders main nav call CTA on mobile when showMobileSliderMenu is false", () => {
|
|
909
|
-
render(
|
|
910
|
-
<Navigation
|
|
911
|
-
{...defaultProps}
|
|
912
|
-
showMobileSliderMenu={false}
|
|
913
|
-
showCallButton={true}
|
|
914
|
-
invocaPhoneNumberLink="tel:1234567890"
|
|
915
|
-
invocaPhoneNumberDisplayText="123-456-7890"
|
|
916
|
-
/>
|
|
917
|
-
);
|
|
918
|
-
const ctaLink = screen
|
|
919
|
-
.getAllByTestId("link")
|
|
920
|
-
.find(el => el.getAttribute("aria-label") === "Call 123-456-7890");
|
|
921
|
-
expect(ctaLink).toBeInTheDocument();
|
|
922
|
-
});
|
|
923
|
-
});
|
|
924
|
-
});
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Navigation } from "./index";
|
|
5
|
+
import { NavigationProps } from "./types";
|
|
6
|
+
|
|
7
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
8
|
+
|
|
9
|
+
jest.mock("./desktop-link-groups.tsx", () => ({
|
|
10
|
+
DesktopLinkGroups: ({ anchorName, link }: any) => (
|
|
11
|
+
<div data-testid={`desktop-link-group-${anchorName}`}>
|
|
12
|
+
{link?.title || link?.buttonLabel}
|
|
13
|
+
</div>
|
|
14
|
+
),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
jest.mock("./mobile-link-groups.tsx", () => ({
|
|
18
|
+
MobileLinkGroups: ({ link }: any) => (
|
|
19
|
+
<div data-testid="mobile-link-group">
|
|
20
|
+
{link?.title || link?.buttonLabel}
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
jest.mock("@shared/components/call-button", () => ({
|
|
26
|
+
CallButton: ({ children, href, onClick, className }: any) => (
|
|
27
|
+
<a
|
|
28
|
+
data-testid="call-button"
|
|
29
|
+
href={href}
|
|
30
|
+
className={className}
|
|
31
|
+
onClick={onClick}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</a>
|
|
35
|
+
),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
jest.mock("@shared/components/input", () => ({
|
|
39
|
+
// eslint-disable-next-line react/display-name
|
|
40
|
+
Input: React.forwardRef(
|
|
41
|
+
(
|
|
42
|
+
{
|
|
43
|
+
name,
|
|
44
|
+
value,
|
|
45
|
+
onChange,
|
|
46
|
+
placeholder,
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
48
|
+
className,
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
50
|
+
containerClassName,
|
|
51
|
+
}: any,
|
|
52
|
+
ref: any
|
|
53
|
+
) => (
|
|
54
|
+
<input
|
|
55
|
+
data-testid={`input-${name}`}
|
|
56
|
+
ref={ref}
|
|
57
|
+
name={name}
|
|
58
|
+
value={value}
|
|
59
|
+
onChange={onChange}
|
|
60
|
+
placeholder={placeholder}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
jest.mock("@shared/components/link", () => ({
|
|
67
|
+
Link: ({ children, href, className, onClick, ...rest }: any) => (
|
|
68
|
+
<a
|
|
69
|
+
data-testid="link"
|
|
70
|
+
href={href}
|
|
71
|
+
className={className}
|
|
72
|
+
onClick={onClick}
|
|
73
|
+
{...rest}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</a>
|
|
77
|
+
),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
jest.mock("@shared/components/material-icon", () => ({
|
|
81
|
+
MaterialIcon: ({ name }: any) => (
|
|
82
|
+
<span data-testid={`icon-${name}`}>{name}</span>
|
|
83
|
+
),
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
jest.mock("@shared/components/next-image", () => ({
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
88
|
+
NextImage: ({ src, alt, onClick, ...rest }: any) => (
|
|
89
|
+
<img data-testid="next-image" src={src} alt={alt} onClick={onClick} />
|
|
90
|
+
),
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
jest.mock("@shared/components/text", () => ({
|
|
94
|
+
Text: ({ as: Tag = "span", children, className }: any) => (
|
|
95
|
+
<Tag className={className}>{children}</Tag>
|
|
96
|
+
),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
jest.mock("@shared/contentful/blocks/button", () => ({
|
|
100
|
+
Button: ({
|
|
101
|
+
children,
|
|
102
|
+
buttonClassName,
|
|
103
|
+
onClick,
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
105
|
+
showButtonAs,
|
|
106
|
+
linkClassName,
|
|
107
|
+
...rest
|
|
108
|
+
}: any) => (
|
|
109
|
+
<button
|
|
110
|
+
data-testid="contentful-button"
|
|
111
|
+
className={buttonClassName || linkClassName}
|
|
112
|
+
onClick={onClick}
|
|
113
|
+
>
|
|
114
|
+
{children || rest.buttonLabel}
|
|
115
|
+
</button>
|
|
116
|
+
),
|
|
117
|
+
__esModule: true,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
jest.mock("@shared/utils", () => ({
|
|
121
|
+
cx: (...args: any[]) => args.filter(Boolean).join(" "),
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
const defaultProps: NavigationProps = {
|
|
125
|
+
primaryNavigationLinks: [],
|
|
126
|
+
utilityNavigationLinks: [],
|
|
127
|
+
primaryNavigationLogo: "https://logo.test/logo.png",
|
|
128
|
+
accountNavigationLinks: [],
|
|
129
|
+
supportNavigationLinks: [],
|
|
130
|
+
searchBarIcon: "https://icon.test/search.png",
|
|
131
|
+
invocaPhoneNumberLink: "tel:1234567890",
|
|
132
|
+
invocaPhoneNumberDisplayText: "123-456-7890",
|
|
133
|
+
callNowCtaText: "Call Now",
|
|
134
|
+
displayCartIcon: false,
|
|
135
|
+
cartHref: "/cart",
|
|
136
|
+
cartHasRetention: false,
|
|
137
|
+
cartIconAriaLabel: "Cart",
|
|
138
|
+
onSearch: jest.fn(),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Props that include primaryNavigationLinks so MobileMenu renders
|
|
142
|
+
const mobileMenuProps: NavigationProps = {
|
|
143
|
+
...defaultProps,
|
|
144
|
+
primaryNavigationLinks: [
|
|
145
|
+
{ buttonLabel: "Products", href: "/products" },
|
|
146
|
+
] as any,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
describe("Navigation", () => {
|
|
150
|
+
beforeEach(() => jest.clearAllMocks());
|
|
151
|
+
|
|
152
|
+
describe("Basic rendering", () => {
|
|
153
|
+
it("renders nav with utility and main nav sections", () => {
|
|
154
|
+
const { container } = render(<Navigation {...defaultProps} />);
|
|
155
|
+
expect(container.querySelector(".menu-container")).toBeInTheDocument();
|
|
156
|
+
expect(container.querySelector(".utility-container")).toBeInTheDocument();
|
|
157
|
+
expect(
|
|
158
|
+
container.querySelector(".desktop-nav-section")
|
|
159
|
+
).toBeInTheDocument();
|
|
160
|
+
expect(
|
|
161
|
+
container.querySelector(".mobile-nav-section")
|
|
162
|
+
).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("renders logo with string src", () => {
|
|
166
|
+
render(<Navigation {...defaultProps} />);
|
|
167
|
+
const logos = screen.getAllByAltText("Kinetic business logo");
|
|
168
|
+
expect(logos[0]).toHaveAttribute("src", "https://logo.test/logo.png");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("renders logo with object src (Asset)", () => {
|
|
172
|
+
render(
|
|
173
|
+
<Navigation
|
|
174
|
+
{...defaultProps}
|
|
175
|
+
primaryNavigationLogo={{ url: "https://logo.test/obj.png" } as any}
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
const logos = screen.getAllByAltText("Kinetic business logo");
|
|
179
|
+
expect(logos[0]).toHaveAttribute("src", "https://logo.test/obj.png");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("does not render logo when logo url is empty", () => {
|
|
183
|
+
const { container } = render(
|
|
184
|
+
<Navigation {...defaultProps} primaryNavigationLogo={{} as any} />
|
|
185
|
+
);
|
|
186
|
+
// logoUrl is "" so the conditional renders null
|
|
187
|
+
expect(
|
|
188
|
+
container.querySelector("[alt='Kinetic business logo']")
|
|
189
|
+
).not.toBeInTheDocument();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("renders phone number display text", () => {
|
|
193
|
+
render(<Navigation {...defaultProps} />);
|
|
194
|
+
expect(screen.getAllByText("123-456-7890").length).toBeGreaterThan(0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("Utility navigation links", () => {
|
|
199
|
+
it("renders utility nav links", () => {
|
|
200
|
+
const links = [
|
|
201
|
+
{ buttonLabel: "Residential", href: "/residential" },
|
|
202
|
+
{ buttonLabel: "Business", href: "/business" },
|
|
203
|
+
];
|
|
204
|
+
render(
|
|
205
|
+
<Navigation {...defaultProps} utilityNavigationLinks={links as any} />
|
|
206
|
+
);
|
|
207
|
+
const buttons = screen.getAllByTestId("contentful-button");
|
|
208
|
+
expect(buttons.length).toBeGreaterThanOrEqual(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("applies active class to second link by default (no utilityNavActiveIndex)", () => {
|
|
212
|
+
const links = [
|
|
213
|
+
{ buttonLabel: "Residential", href: "/residential" },
|
|
214
|
+
{ buttonLabel: "Business", href: "/business" },
|
|
215
|
+
];
|
|
216
|
+
const { container } = render(
|
|
217
|
+
<Navigation {...defaultProps} utilityNavigationLinks={links as any} />
|
|
218
|
+
);
|
|
219
|
+
const utilityButtons = container.querySelectorAll(
|
|
220
|
+
".utility-container button"
|
|
221
|
+
);
|
|
222
|
+
expect(utilityButtons[1]?.className).toContain("label4");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("applies active class based on utilityNavActiveIndex", () => {
|
|
226
|
+
const links = [
|
|
227
|
+
{ buttonLabel: "Residential", href: "/residential" },
|
|
228
|
+
{ buttonLabel: "Business", href: "/business" },
|
|
229
|
+
];
|
|
230
|
+
const { container } = render(
|
|
231
|
+
<Navigation
|
|
232
|
+
{...defaultProps}
|
|
233
|
+
utilityNavigationLinks={links as any}
|
|
234
|
+
utilityNavActiveIndex={0}
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
const utilityButtons = container.querySelectorAll(
|
|
238
|
+
".utility-container button"
|
|
239
|
+
);
|
|
240
|
+
expect(utilityButtons[0]?.className).toContain("label4");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("Cart icon", () => {
|
|
245
|
+
it("renders cart icon when displayCartIcon is true", () => {
|
|
246
|
+
render(<Navigation {...defaultProps} displayCartIcon={true} />);
|
|
247
|
+
expect(
|
|
248
|
+
screen.getAllByTestId("icon-shopping_cart").length
|
|
249
|
+
).toBeGreaterThan(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("shows retention dot when cartHasRetention is true", () => {
|
|
253
|
+
const { container } = render(
|
|
254
|
+
<Navigation
|
|
255
|
+
{...defaultProps}
|
|
256
|
+
displayCartIcon={true}
|
|
257
|
+
cartHasRetention={true}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
expect(container.querySelector(".bg-icon-brand")).toBeInTheDocument();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("does not render cart icon when displayCartIcon is false", () => {
|
|
264
|
+
render(<Navigation {...defaultProps} displayCartIcon={false} />);
|
|
265
|
+
expect(
|
|
266
|
+
screen.queryByTestId("icon-shopping_cart")
|
|
267
|
+
).not.toBeInTheDocument();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("calls onCartClick when cart is clicked", () => {
|
|
271
|
+
const onCartClick = jest.fn();
|
|
272
|
+
render(
|
|
273
|
+
<Navigation
|
|
274
|
+
{...defaultProps}
|
|
275
|
+
displayCartIcon={true}
|
|
276
|
+
onCartClick={onCartClick}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
const cartLinks = screen.getAllByLabelText("Cart");
|
|
280
|
+
fireEvent.click(cartLinks[0]);
|
|
281
|
+
expect(onCartClick).toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("Primary navigation links", () => {
|
|
286
|
+
it("renders DesktopLinkGroups for primary nav links", () => {
|
|
287
|
+
const links = [{ title: "Products", items: { items: [] } }];
|
|
288
|
+
render(
|
|
289
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
290
|
+
);
|
|
291
|
+
expect(
|
|
292
|
+
screen.getByTestId("desktop-link-group-main-menu-0")
|
|
293
|
+
).toBeInTheDocument();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe("Account navigation links", () => {
|
|
298
|
+
it("renders DesktopLinkGroups for account links", () => {
|
|
299
|
+
const links = [{ title: "My Account", items: { items: [] } }];
|
|
300
|
+
render(
|
|
301
|
+
<Navigation {...defaultProps} accountNavigationLinks={links as any} />
|
|
302
|
+
);
|
|
303
|
+
expect(
|
|
304
|
+
screen.getByTestId("desktop-link-group-my-account-0")
|
|
305
|
+
).toBeInTheDocument();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("Support navigation links", () => {
|
|
310
|
+
it("renders DesktopLinkGroups for support links", () => {
|
|
311
|
+
const links = [{ title: "Support", items: { items: [] } }];
|
|
312
|
+
render(
|
|
313
|
+
<Navigation {...defaultProps} supportNavigationLinks={links as any} />
|
|
314
|
+
);
|
|
315
|
+
expect(
|
|
316
|
+
screen.getByTestId("desktop-link-group-support-menu-0")
|
|
317
|
+
).toBeInTheDocument();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("Search", () => {
|
|
322
|
+
it("renders desktop search input", () => {
|
|
323
|
+
render(<Navigation {...defaultProps} />);
|
|
324
|
+
expect(
|
|
325
|
+
screen.getAllByPlaceholderText("Search...").length
|
|
326
|
+
).toBeGreaterThan(0);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("calls onSearch when desktop search form is submitted", () => {
|
|
330
|
+
const onSearch = jest.fn();
|
|
331
|
+
const { container } = render(
|
|
332
|
+
<Navigation {...defaultProps} onSearch={onSearch} />
|
|
333
|
+
);
|
|
334
|
+
const desktopForm = container.querySelector(".desktop-nav-section form");
|
|
335
|
+
const input = desktopForm?.querySelector("input");
|
|
336
|
+
fireEvent.change(input!, { target: { value: "test query" } });
|
|
337
|
+
fireEvent.submit(desktopForm!);
|
|
338
|
+
expect(onSearch).toHaveBeenCalledWith("test query");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("calls onSearch when search icon is clicked", () => {
|
|
342
|
+
const onSearch = jest.fn();
|
|
343
|
+
const { container } = render(
|
|
344
|
+
<Navigation
|
|
345
|
+
{...defaultProps}
|
|
346
|
+
onSearch={onSearch}
|
|
347
|
+
searchBarIcon="https://icon.test/s.png"
|
|
348
|
+
/>
|
|
349
|
+
);
|
|
350
|
+
const desktopForm = container.querySelector(".desktop-nav-section form");
|
|
351
|
+
const input = desktopForm?.querySelector("input");
|
|
352
|
+
fireEvent.change(input!, { target: { value: "click search" } });
|
|
353
|
+
const searchIcon = desktopForm?.querySelector("img");
|
|
354
|
+
fireEvent.click(searchIcon!);
|
|
355
|
+
expect(onSearch).toHaveBeenCalledWith("click search");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("uses searchBarIcon url from object", () => {
|
|
359
|
+
render(
|
|
360
|
+
<Navigation
|
|
361
|
+
{...defaultProps}
|
|
362
|
+
searchBarIcon={{ url: "https://icon.test/obj.png" } as any}
|
|
363
|
+
/>
|
|
364
|
+
);
|
|
365
|
+
const imgs = screen.getAllByAltText("Search icon");
|
|
366
|
+
expect(imgs[0]).toHaveAttribute("src", "https://icon.test/obj.png");
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("Mobile menu", () => {
|
|
371
|
+
it("does not render mobile menu when no nav links provided", () => {
|
|
372
|
+
const { container } = render(<Navigation {...defaultProps} />);
|
|
373
|
+
const menuButton = container.querySelector(
|
|
374
|
+
".mobile-nav-section [data-testid='icon-menu']"
|
|
375
|
+
);
|
|
376
|
+
expect(menuButton).not.toBeInTheDocument();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("opens mobile menu on hamburger click", () => {
|
|
380
|
+
const { container } = render(<Navigation {...mobileMenuProps} />);
|
|
381
|
+
const menuButton = container
|
|
382
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
383
|
+
?.closest("button");
|
|
384
|
+
fireEvent.click(menuButton!);
|
|
385
|
+
expect(
|
|
386
|
+
container.querySelector("#mobile-menu-overlay")
|
|
387
|
+
).toBeInTheDocument();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("closes mobile menu on close button click", () => {
|
|
391
|
+
const { container } = render(<Navigation {...mobileMenuProps} />);
|
|
392
|
+
const menuButton = container
|
|
393
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
394
|
+
?.closest("button");
|
|
395
|
+
fireEvent.click(menuButton!);
|
|
396
|
+
const closeButton = container
|
|
397
|
+
.querySelector("#drawer-items [data-testid='icon-close']")
|
|
398
|
+
?.closest("button");
|
|
399
|
+
fireEvent.click(closeButton!);
|
|
400
|
+
expect(
|
|
401
|
+
container.querySelector("#mobile-menu-overlay")?.className
|
|
402
|
+
).toContain("-right-96");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("renders mobile search input and submits", () => {
|
|
406
|
+
const onSearch = jest.fn();
|
|
407
|
+
const { container } = render(
|
|
408
|
+
<Navigation {...mobileMenuProps} onSearch={onSearch} />
|
|
409
|
+
);
|
|
410
|
+
const menuButton = container
|
|
411
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
412
|
+
?.closest("button");
|
|
413
|
+
fireEvent.click(menuButton!);
|
|
414
|
+
const mobileForm = container.querySelector("#drawer-items form");
|
|
415
|
+
const input = mobileForm?.querySelector("input");
|
|
416
|
+
fireEvent.change(input!, { target: { value: "mobile search" } });
|
|
417
|
+
fireEvent.submit(mobileForm!);
|
|
418
|
+
expect(onSearch).toHaveBeenCalledWith("mobile search");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("closes menu when a link is clicked in the drawer", () => {
|
|
422
|
+
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
423
|
+
const { container } = render(
|
|
424
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
425
|
+
);
|
|
426
|
+
const menuButton = container
|
|
427
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
428
|
+
?.closest("button");
|
|
429
|
+
fireEvent.click(menuButton!);
|
|
430
|
+
expect(
|
|
431
|
+
container.querySelector("#mobile-menu-overlay")?.className
|
|
432
|
+
).toContain("right-0");
|
|
433
|
+
expect(
|
|
434
|
+
container.querySelector("#mobile-menu-overlay")?.className
|
|
435
|
+
).not.toContain("-right-96");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("renders MobileLinkGroups in drawer", () => {
|
|
439
|
+
const links = [{ title: "Products", items: { items: [] } }];
|
|
440
|
+
const { container } = render(
|
|
441
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
442
|
+
);
|
|
443
|
+
const menuButton = container
|
|
444
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
445
|
+
?.closest("button");
|
|
446
|
+
fireEvent.click(menuButton!);
|
|
447
|
+
expect(screen.getAllByTestId("mobile-link-group").length).toBeGreaterThan(
|
|
448
|
+
0
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("closes menu when an anchor inside the drawer content is clicked", () => {
|
|
453
|
+
const links = [{ buttonLabel: "Internet", href: "/internet" }];
|
|
454
|
+
const { container } = render(
|
|
455
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
456
|
+
);
|
|
457
|
+
const menuButton = container
|
|
458
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
459
|
+
?.closest("button");
|
|
460
|
+
fireEvent.click(menuButton!);
|
|
461
|
+
expect(
|
|
462
|
+
container.querySelector("#mobile-menu-overlay")?.className
|
|
463
|
+
).toContain("right-0");
|
|
464
|
+
const scrollDiv = container.querySelector(
|
|
465
|
+
"#drawer-items .flex-grow.overflow-y-auto"
|
|
466
|
+
);
|
|
467
|
+
const anchor = document.createElement("a");
|
|
468
|
+
anchor.href = "/internet";
|
|
469
|
+
scrollDiv!.appendChild(anchor);
|
|
470
|
+
fireEvent.click(anchor);
|
|
471
|
+
expect(
|
|
472
|
+
container.querySelector("#mobile-menu-overlay")?.className
|
|
473
|
+
).toContain("-right-96");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("does not close menu when non-anchor element is clicked in drawer", () => {
|
|
477
|
+
const links = [{ title: "Products", items: { items: [] } }];
|
|
478
|
+
const { container } = render(
|
|
479
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
480
|
+
);
|
|
481
|
+
const menuButton = container
|
|
482
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
483
|
+
?.closest("button");
|
|
484
|
+
fireEvent.click(menuButton!);
|
|
485
|
+
const scrollDiv = container.querySelector(
|
|
486
|
+
"#drawer-items .flex-grow.overflow-y-auto"
|
|
487
|
+
);
|
|
488
|
+
fireEvent.click(scrollDiv!);
|
|
489
|
+
expect(
|
|
490
|
+
container.querySelector("#mobile-menu-overlay")?.className
|
|
491
|
+
).toContain("right-0");
|
|
492
|
+
expect(
|
|
493
|
+
container.querySelector("#mobile-menu-overlay")?.className
|
|
494
|
+
).not.toContain("-right-96");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("traps focus with Tab key - wraps from last to first", () => {
|
|
498
|
+
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
499
|
+
const { container } = render(
|
|
500
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
501
|
+
);
|
|
502
|
+
const menuButton = container
|
|
503
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
504
|
+
?.closest("button");
|
|
505
|
+
fireEvent.click(menuButton!);
|
|
506
|
+
|
|
507
|
+
const drawerItems = container.querySelector("#drawer-items");
|
|
508
|
+
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
509
|
+
const firstEl = focusableEls[0] as HTMLElement;
|
|
510
|
+
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
|
|
511
|
+
|
|
512
|
+
lastEl.focus();
|
|
513
|
+
expect(document.activeElement).toBe(lastEl);
|
|
514
|
+
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: false });
|
|
515
|
+
expect(document.activeElement).toBe(firstEl);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("traps focus with Shift+Tab - wraps from first to last", () => {
|
|
519
|
+
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
520
|
+
const { container } = render(
|
|
521
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
522
|
+
);
|
|
523
|
+
const menuButton = container
|
|
524
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
525
|
+
?.closest("button");
|
|
526
|
+
fireEvent.click(menuButton!);
|
|
527
|
+
|
|
528
|
+
const drawerItems = container.querySelector("#drawer-items");
|
|
529
|
+
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
530
|
+
const firstEl = focusableEls[0] as HTMLElement;
|
|
531
|
+
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
|
|
532
|
+
|
|
533
|
+
firstEl.focus();
|
|
534
|
+
expect(document.activeElement).toBe(firstEl);
|
|
535
|
+
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: true });
|
|
536
|
+
expect(document.activeElement).toBe(lastEl);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("does not trap focus for non-Tab keys", () => {
|
|
540
|
+
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
541
|
+
const { container } = render(
|
|
542
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
543
|
+
);
|
|
544
|
+
const menuButton = container
|
|
545
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
546
|
+
?.closest("button");
|
|
547
|
+
fireEvent.click(menuButton!);
|
|
548
|
+
|
|
549
|
+
const drawerItems = container.querySelector("#drawer-items");
|
|
550
|
+
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
551
|
+
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
|
|
552
|
+
lastEl.focus();
|
|
553
|
+
|
|
554
|
+
fireEvent.keyDown(window, { key: "Enter", keyCode: 13 });
|
|
555
|
+
expect(document.activeElement).toBe(lastEl);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("hides mobile call button when hideMobileCallButton is true", () => {
|
|
559
|
+
const { container } = render(
|
|
560
|
+
<Navigation {...mobileMenuProps} hideMobileCallButton={true} />
|
|
561
|
+
);
|
|
562
|
+
const mobileGapDiv = container.querySelector(
|
|
563
|
+
".mobile-nav-section .flex.items-center.gap-6"
|
|
564
|
+
);
|
|
565
|
+
const directCallButtons = mobileGapDiv?.querySelectorAll(
|
|
566
|
+
":scope > [data-testid='call-button']"
|
|
567
|
+
);
|
|
568
|
+
expect(directCallButtons?.length || 0).toBe(0);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("shows mobile call button by default", () => {
|
|
572
|
+
const { container } = render(<Navigation {...mobileMenuProps} />);
|
|
573
|
+
const mobileSection = container.querySelector(".mobile-nav-section");
|
|
574
|
+
const callButton = mobileSection?.querySelector(
|
|
575
|
+
"[data-testid='call-button']"
|
|
576
|
+
);
|
|
577
|
+
expect(callButton).toBeInTheDocument();
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
describe("checkPlansJSX", () => {
|
|
582
|
+
it("renders checkPlansJSX when provided", () => {
|
|
583
|
+
render(
|
|
584
|
+
<Navigation
|
|
585
|
+
{...defaultProps}
|
|
586
|
+
checkPlansJSX={<div data-testid="check-plans">Plans</div>}
|
|
587
|
+
/>
|
|
588
|
+
);
|
|
589
|
+
expect(screen.getByTestId("check-plans")).toBeInTheDocument();
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("does not render checkPlansJSX when not provided", () => {
|
|
593
|
+
render(<Navigation {...defaultProps} />);
|
|
594
|
+
expect(screen.queryByTestId("check-plans")).not.toBeInTheDocument();
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe("Logo dimensions", () => {
|
|
599
|
+
it("uses custom logo dimensions", () => {
|
|
600
|
+
render(
|
|
601
|
+
<Navigation
|
|
602
|
+
{...defaultProps}
|
|
603
|
+
primaryNavigationLogoWidth={100}
|
|
604
|
+
primaryNavigationLogoHeight={32}
|
|
605
|
+
/>
|
|
606
|
+
);
|
|
607
|
+
const logos = screen.getAllByAltText("Kinetic business logo");
|
|
608
|
+
expect(logos.length).toBeGreaterThan(0);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("renders logo from object src in mobile section", () => {
|
|
612
|
+
render(
|
|
613
|
+
<Navigation
|
|
614
|
+
{...defaultProps}
|
|
615
|
+
primaryNavigationLogo={{ url: "https://logo.test/m.png" } as any}
|
|
616
|
+
/>
|
|
617
|
+
);
|
|
618
|
+
const logos = screen.getAllByAltText("Kinetic business logo");
|
|
619
|
+
expect(logos[0]).toHaveAttribute("src", "https://logo.test/m.png");
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
describe("Null/undefined optional arrays (branch coverage)", () => {
|
|
624
|
+
it("renders without crashing when utilityNavigationLinks is undefined", () => {
|
|
625
|
+
const { container } = render(
|
|
626
|
+
<Navigation
|
|
627
|
+
{...defaultProps}
|
|
628
|
+
utilityNavigationLinks={undefined as any}
|
|
629
|
+
/>
|
|
630
|
+
);
|
|
631
|
+
expect(container.querySelector(".menu-container")).toBeInTheDocument();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("renders without crashing when primaryNavigationLinks is undefined", () => {
|
|
635
|
+
const { container } = render(
|
|
636
|
+
<Navigation
|
|
637
|
+
{...defaultProps}
|
|
638
|
+
primaryNavigationLinks={undefined as any}
|
|
639
|
+
/>
|
|
640
|
+
);
|
|
641
|
+
expect(
|
|
642
|
+
container.querySelector(".desktop-nav-section")
|
|
643
|
+
).toBeInTheDocument();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("renders without crashing when accountNavigationLinks is undefined", () => {
|
|
647
|
+
const { container } = render(
|
|
648
|
+
<Navigation
|
|
649
|
+
{...defaultProps}
|
|
650
|
+
accountNavigationLinks={undefined as any}
|
|
651
|
+
/>
|
|
652
|
+
);
|
|
653
|
+
expect(container.querySelector(".menu-container")).toBeInTheDocument();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("renders without crashing when supportNavigationLinks is undefined", () => {
|
|
657
|
+
const { container } = render(
|
|
658
|
+
<Navigation
|
|
659
|
+
{...defaultProps}
|
|
660
|
+
supportNavigationLinks={undefined as any}
|
|
661
|
+
/>
|
|
662
|
+
);
|
|
663
|
+
expect(container.querySelector(".menu-container")).toBeInTheDocument();
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
describe("Mobile cart icon", () => {
|
|
668
|
+
it("renders cart icon in mobile section when displayCartIcon is true", () => {
|
|
669
|
+
const { container } = render(
|
|
670
|
+
<Navigation {...mobileMenuProps} displayCartIcon={true} />
|
|
671
|
+
);
|
|
672
|
+
const mobileSection = container.querySelector(".mobile-nav-section");
|
|
673
|
+
expect(
|
|
674
|
+
mobileSection?.querySelector("[data-testid='icon-shopping_cart']")
|
|
675
|
+
).toBeInTheDocument();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("shows retention dot on mobile cart when cartHasRetention is true", () => {
|
|
679
|
+
const { container } = render(
|
|
680
|
+
<Navigation
|
|
681
|
+
{...mobileMenuProps}
|
|
682
|
+
displayCartIcon={true}
|
|
683
|
+
cartHasRetention={true}
|
|
684
|
+
/>
|
|
685
|
+
);
|
|
686
|
+
const mobileSection = container.querySelector(".mobile-nav-section");
|
|
687
|
+
expect(
|
|
688
|
+
mobileSection?.querySelector(".bg-icon-brand")
|
|
689
|
+
).toBeInTheDocument();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("does not show retention dot on mobile when cartHasRetention is false", () => {
|
|
693
|
+
const { container } = render(
|
|
694
|
+
<Navigation
|
|
695
|
+
{...mobileMenuProps}
|
|
696
|
+
displayCartIcon={true}
|
|
697
|
+
cartHasRetention={false}
|
|
698
|
+
/>
|
|
699
|
+
);
|
|
700
|
+
const mobileSection = container.querySelector(".mobile-nav-section");
|
|
701
|
+
const dots = mobileSection?.querySelectorAll(".bg-icon-brand");
|
|
702
|
+
expect(dots?.length || 0).toBe(0);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("uses fallback '#' when cartHref is undefined", () => {
|
|
706
|
+
const { container } = render(
|
|
707
|
+
<Navigation
|
|
708
|
+
{...mobileMenuProps}
|
|
709
|
+
displayCartIcon={true}
|
|
710
|
+
cartHref={undefined as any}
|
|
711
|
+
/>
|
|
712
|
+
);
|
|
713
|
+
const cartLinks = container.querySelectorAll("[aria-label='Cart']");
|
|
714
|
+
cartLinks.forEach(link => {
|
|
715
|
+
expect(link).toHaveAttribute("href", "#");
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
describe("Mobile drawer utility links", () => {
|
|
721
|
+
it("renders utility links in mobile drawer", () => {
|
|
722
|
+
const links = [
|
|
723
|
+
{ buttonLabel: "Residential", href: "/residential", anchorId: "r1" },
|
|
724
|
+
{ buttonLabel: "Business", href: "/business", anchorId: "b1" },
|
|
725
|
+
];
|
|
726
|
+
const { container } = render(
|
|
727
|
+
<Navigation
|
|
728
|
+
{...mobileMenuProps}
|
|
729
|
+
utilityNavigationLinks={links as any}
|
|
730
|
+
/>
|
|
731
|
+
);
|
|
732
|
+
const menuButton = container
|
|
733
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
734
|
+
?.closest("button");
|
|
735
|
+
fireEvent.click(menuButton!);
|
|
736
|
+
const drawerButtons = container.querySelectorAll(
|
|
737
|
+
"#drawer-items .bg-bg-fill-info button"
|
|
738
|
+
);
|
|
739
|
+
expect(drawerButtons.length).toBe(2);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("applies active class to utility links in drawer using utilityNavActiveIndex", () => {
|
|
743
|
+
const links = [
|
|
744
|
+
{ buttonLabel: "Residential", href: "/residential", anchorId: "r1" },
|
|
745
|
+
{ buttonLabel: "Business", href: "/business", anchorId: "b1" },
|
|
746
|
+
];
|
|
747
|
+
const { container } = render(
|
|
748
|
+
<Navigation
|
|
749
|
+
{...mobileMenuProps}
|
|
750
|
+
utilityNavigationLinks={links as any}
|
|
751
|
+
utilityNavActiveIndex={0}
|
|
752
|
+
/>
|
|
753
|
+
);
|
|
754
|
+
const menuButton = container
|
|
755
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
756
|
+
?.closest("button");
|
|
757
|
+
fireEvent.click(menuButton!);
|
|
758
|
+
const drawerButtons = container.querySelectorAll(
|
|
759
|
+
"#drawer-items .bg-bg-fill-info button"
|
|
760
|
+
);
|
|
761
|
+
expect(drawerButtons[0]?.className).toContain("label4");
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("handles undefined utilityNavigationLinks in mobile drawer", () => {
|
|
765
|
+
const { container } = render(
|
|
766
|
+
<Navigation
|
|
767
|
+
{...mobileMenuProps}
|
|
768
|
+
utilityNavigationLinks={undefined as any}
|
|
769
|
+
/>
|
|
770
|
+
);
|
|
771
|
+
const menuButton = container
|
|
772
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
773
|
+
?.closest("button");
|
|
774
|
+
fireEvent.click(menuButton!);
|
|
775
|
+
const drawerButtons = container.querySelectorAll(
|
|
776
|
+
"#drawer-items .bg-bg-fill-info button"
|
|
777
|
+
);
|
|
778
|
+
expect(drawerButtons.length).toBe(0);
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
describe("Mobile search with object searchBarIcon", () => {
|
|
783
|
+
it("uses searchBarIcon url from object in mobile menu", () => {
|
|
784
|
+
const { container } = render(
|
|
785
|
+
<Navigation
|
|
786
|
+
{...mobileMenuProps}
|
|
787
|
+
searchBarIcon={{ url: "https://icon.test/mobile.png" } as any}
|
|
788
|
+
/>
|
|
789
|
+
);
|
|
790
|
+
const menuButton = container
|
|
791
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
792
|
+
?.closest("button");
|
|
793
|
+
fireEvent.click(menuButton!);
|
|
794
|
+
const mobileForm = container.querySelector("#drawer-items form");
|
|
795
|
+
const img = mobileForm?.querySelector("img");
|
|
796
|
+
expect(img).toHaveAttribute("src", "https://icon.test/mobile.png");
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
describe("Focus trap edge cases", () => {
|
|
801
|
+
it("does not wrap focus when Tab pressed and not on last element", () => {
|
|
802
|
+
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
803
|
+
const { container } = render(
|
|
804
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
805
|
+
);
|
|
806
|
+
const menuButton = container
|
|
807
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
808
|
+
?.closest("button");
|
|
809
|
+
fireEvent.click(menuButton!);
|
|
810
|
+
|
|
811
|
+
const drawerItems = container.querySelector("#drawer-items");
|
|
812
|
+
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
813
|
+
const firstEl = focusableEls[0] as HTMLElement;
|
|
814
|
+
|
|
815
|
+
firstEl.focus();
|
|
816
|
+
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: false });
|
|
817
|
+
expect(document.activeElement).toBe(firstEl);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("does not wrap focus when Shift+Tab pressed and not on first element", () => {
|
|
821
|
+
const links = [{ buttonLabel: "Link1", href: "/link1" }];
|
|
822
|
+
const { container } = render(
|
|
823
|
+
<Navigation {...defaultProps} primaryNavigationLinks={links as any} />
|
|
824
|
+
);
|
|
825
|
+
const menuButton = container
|
|
826
|
+
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
|
|
827
|
+
?.closest("button");
|
|
828
|
+
fireEvent.click(menuButton!);
|
|
829
|
+
|
|
830
|
+
const drawerItems = container.querySelector("#drawer-items");
|
|
831
|
+
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
|
|
832
|
+
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
|
|
833
|
+
|
|
834
|
+
lastEl.focus();
|
|
835
|
+
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: true });
|
|
836
|
+
expect(document.activeElement).toBe(lastEl);
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
describe("Navigation background color", () => {
|
|
841
|
+
it("applies bg-secondary-navy500 by default", () => {
|
|
842
|
+
const { container } = render(<Navigation {...defaultProps} />);
|
|
843
|
+
const mainNav = container.querySelector(".main-nav-container");
|
|
844
|
+
expect(mainNav?.className).toContain("bg-secondary-navy500");
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it("applies custom bg class with token format", () => {
|
|
848
|
+
const { container } = render(
|
|
849
|
+
<Navigation {...defaultProps} navigationBackgroundColor="green300" />
|
|
850
|
+
);
|
|
851
|
+
const mainNav = container.querySelector(".main-nav-container");
|
|
852
|
+
expect(mainNav?.className).toContain("bg-secondary-green300");
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("passes through raw tailwind class", () => {
|
|
856
|
+
const { container } = render(
|
|
857
|
+
<Navigation {...defaultProps} navigationBackgroundColor="bg-blue-500" />
|
|
858
|
+
);
|
|
859
|
+
const mainNav = container.querySelector(".main-nav-container");
|
|
860
|
+
expect(mainNav?.className).toContain("bg-blue-500");
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
describe("Display options", () => {
|
|
865
|
+
it("hides utility navigation when displayUtilityNavigation is false", () => {
|
|
866
|
+
const { container } = render(
|
|
867
|
+
<Navigation {...defaultProps} displayUtilityNavigation={false} />
|
|
868
|
+
);
|
|
869
|
+
expect(
|
|
870
|
+
container.querySelector(".utility-container")
|
|
871
|
+
).not.toBeInTheDocument();
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("hides search bar when displaySearchBar is false", () => {
|
|
875
|
+
const { container } = render(
|
|
876
|
+
<Navigation {...defaultProps} displaySearchBar={false} />
|
|
877
|
+
);
|
|
878
|
+
const desktopForm = container.querySelector(".desktop-nav-section form");
|
|
879
|
+
expect(desktopForm).not.toBeInTheDocument();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it("hides call now CTA when displayCallNowCta is false", () => {
|
|
883
|
+
const { container } = render(
|
|
884
|
+
<Navigation {...defaultProps} displayCallNowCta={false} />
|
|
885
|
+
);
|
|
886
|
+
const utilityCta = container.querySelector(
|
|
887
|
+
".utility-container [data-testid='call-button']"
|
|
888
|
+
);
|
|
889
|
+
expect(utilityCta).not.toBeInTheDocument();
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it("renders main nav call CTA when showCallNowCtaInMainNav is true", () => {
|
|
893
|
+
render(
|
|
894
|
+
<Navigation
|
|
895
|
+
{...defaultProps}
|
|
896
|
+
showCallNowCtaInMainNav={true}
|
|
897
|
+
displayUtilityNavigation={false}
|
|
898
|
+
invocaPhoneNumberLink="tel:1234567890"
|
|
899
|
+
invocaPhoneNumberDisplayText="123-456-7890"
|
|
900
|
+
/>
|
|
901
|
+
);
|
|
902
|
+
const ctaLink = screen
|
|
903
|
+
.getAllByTestId("link")
|
|
904
|
+
.find(el => el.getAttribute("aria-label") === "Call 123-456-7890");
|
|
905
|
+
expect(ctaLink).toBeInTheDocument();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it("renders main nav call CTA on mobile when showMobileSliderMenu is false", () => {
|
|
909
|
+
render(
|
|
910
|
+
<Navigation
|
|
911
|
+
{...defaultProps}
|
|
912
|
+
showMobileSliderMenu={false}
|
|
913
|
+
showCallButton={true}
|
|
914
|
+
invocaPhoneNumberLink="tel:1234567890"
|
|
915
|
+
invocaPhoneNumberDisplayText="123-456-7890"
|
|
916
|
+
/>
|
|
917
|
+
);
|
|
918
|
+
const ctaLink = screen
|
|
919
|
+
.getAllByTestId("link")
|
|
920
|
+
.find(el => el.getAttribute("aria-label") === "Call 123-456-7890");
|
|
921
|
+
expect(ctaLink).toBeInTheDocument();
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
});
|