@windstream/react-shared-components 0.2.88 → 0.2.90
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/dist/contentful/index.d.ts +14 -0
- package/dist/contentful/index.esm.js +4 -4
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +4 -4
- package/dist/contentful/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/utils/index.esm.js.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +55 -0
- package/src/contentful/blocks/anchored-bottom-banner/index.tsx +15 -2
- package/src/contentful/blocks/anchored-bottom-banner/types.ts +5 -0
- package/src/contentful/blocks/award-marquee/index.test.tsx +75 -0
- package/src/contentful/blocks/award-marquee/index.tsx +40 -19
- package/src/contentful/blocks/award-marquee/types.ts +6 -0
- package/src/contentful/blocks/cart-retention-banner/index.test.tsx +63 -3
- package/src/contentful/blocks/cart-retention-banner/index.tsx +21 -8
- package/src/contentful/blocks/cart-retention-banner/types.ts +4 -0
- package/src/contentful/blocks/floating-banner/index.test.tsx +54 -2
- package/src/contentful/blocks/floating-banner/index.tsx +13 -2
- package/src/contentful/blocks/floating-banner/types.ts +6 -0
- package/src/contentful/blocks/image-promo-bar/index.test.tsx +176 -18
- package/src/contentful/blocks/image-promo-bar/index.tsx +45 -27
- package/src/contentful/blocks/image-promo-bar/types.ts +19 -1
- package/src/contentful/blocks/primary-hero/index.test.tsx +263 -4
- package/src/contentful/blocks/primary-hero/index.tsx +48 -4
- package/src/contentful/blocks/primary-hero/primary-hero.stories.tsx +22 -1
- package/src/utils/replace-tokens.test.ts +34 -0
- package/src/utils/replace-tokens.ts +4 -0
|
@@ -7,22 +7,42 @@ import { render, screen } from "@testing-library/react";
|
|
|
7
7
|
jest.mock("../button", () => ({
|
|
8
8
|
__esModule: true,
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
10
|
-
Button: ({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
Button: ({
|
|
11
|
+
buttonLabel,
|
|
12
|
+
disableAnimation,
|
|
13
|
+
fieldProps,
|
|
14
|
+
preDefinedFunctionExecution,
|
|
15
|
+
renderCheckPlans,
|
|
16
|
+
}: any) =>
|
|
17
|
+
preDefinedFunctionExecution === "check availability" && renderCheckPlans ? (
|
|
18
|
+
renderCheckPlans()
|
|
19
|
+
) : (
|
|
20
|
+
<button
|
|
21
|
+
data-testid="cta-button"
|
|
22
|
+
data-disable-animation={String(disableAnimation)}
|
|
23
|
+
{...fieldProps}
|
|
24
|
+
>
|
|
25
|
+
{buttonLabel || "CTA"}
|
|
26
|
+
</button>
|
|
27
|
+
),
|
|
28
|
+
default: ({
|
|
29
|
+
buttonLabel,
|
|
30
|
+
disableAnimation,
|
|
31
|
+
fieldProps,
|
|
32
|
+
preDefinedFunctionExecution,
|
|
33
|
+
renderCheckPlans,
|
|
34
|
+
}: any) =>
|
|
35
|
+
preDefinedFunctionExecution === "check availability" && renderCheckPlans ? (
|
|
36
|
+
renderCheckPlans()
|
|
37
|
+
) : (
|
|
38
|
+
<button
|
|
39
|
+
data-testid="cta-button"
|
|
40
|
+
data-disable-animation={String(disableAnimation)}
|
|
41
|
+
{...fieldProps}
|
|
42
|
+
>
|
|
43
|
+
{buttonLabel || "CTA"}
|
|
44
|
+
</button>
|
|
45
|
+
),
|
|
26
46
|
}));
|
|
27
47
|
|
|
28
48
|
jest.mock("@shared/components/video-link", () => ({
|
|
@@ -63,8 +83,10 @@ jest.mock("@shared/components/next-image", () => ({
|
|
|
63
83
|
}));
|
|
64
84
|
|
|
65
85
|
jest.mock("@shared/components/text", () => ({
|
|
66
|
-
Text: ({ as: Tag = "span", children, className }: any) => (
|
|
67
|
-
<Tag className={className}
|
|
86
|
+
Text: ({ as: Tag = "span", children, className, ...rest }: any) => (
|
|
87
|
+
<Tag className={className} {...rest}>
|
|
88
|
+
{children}
|
|
89
|
+
</Tag>
|
|
68
90
|
),
|
|
69
91
|
}));
|
|
70
92
|
|
|
@@ -94,6 +116,142 @@ const defaultProps: ImagePromoBarProps = {
|
|
|
94
116
|
};
|
|
95
117
|
|
|
96
118
|
describe("ImagePromoBar", () => {
|
|
119
|
+
it("tags block content, CTAs, disclaimers, checklist and image links", () => {
|
|
120
|
+
const tag = (field: string) => ({
|
|
121
|
+
"data-contentful-field-id": field,
|
|
122
|
+
"data-contentful-entry-id": "image-promo",
|
|
123
|
+
"data-contentful-locale": "en-US",
|
|
124
|
+
});
|
|
125
|
+
const { container } = render(
|
|
126
|
+
<ImagePromoBar
|
|
127
|
+
{...defaultProps}
|
|
128
|
+
brow="Brow"
|
|
129
|
+
title="Title"
|
|
130
|
+
subTitle="Subtitle"
|
|
131
|
+
description="Description"
|
|
132
|
+
checklist={["Check"]}
|
|
133
|
+
ctaDisclaimer="CTA disclaimer"
|
|
134
|
+
disclaimer="Disclaimer"
|
|
135
|
+
imageLinks={[
|
|
136
|
+
{
|
|
137
|
+
url: "/image",
|
|
138
|
+
image: "/image.png",
|
|
139
|
+
fieldProps: {
|
|
140
|
+
image: {
|
|
141
|
+
...tag("image"),
|
|
142
|
+
"data-contentful-entry-id": "image-link-item",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
]}
|
|
147
|
+
cta={{
|
|
148
|
+
buttonLabel: "Primary",
|
|
149
|
+
fieldProps: { "data-contentful-field-id": "nestedPrimaryCta" },
|
|
150
|
+
}}
|
|
151
|
+
secondaryCta={{
|
|
152
|
+
buttonLabel: "Secondary",
|
|
153
|
+
fieldProps: { "data-contentful-field-id": "nestedSecondaryCta" },
|
|
154
|
+
}}
|
|
155
|
+
fieldProps={{
|
|
156
|
+
brow: tag("brow"),
|
|
157
|
+
title: tag("title"),
|
|
158
|
+
subTitle: tag("subTitle"),
|
|
159
|
+
description: tag("description"),
|
|
160
|
+
checklist: tag("checklist"),
|
|
161
|
+
ctaDisclaimer: tag("ctaDisclaimer"),
|
|
162
|
+
cta: tag("cta"),
|
|
163
|
+
secondaryCta: tag("secondaryCta"),
|
|
164
|
+
disclaimer: tag("disclaimer"),
|
|
165
|
+
imageLinks: tag("imageLinks"),
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
[
|
|
170
|
+
"brow",
|
|
171
|
+
"title",
|
|
172
|
+
"subTitle",
|
|
173
|
+
"description",
|
|
174
|
+
"checklist",
|
|
175
|
+
"ctaDisclaimer",
|
|
176
|
+
"cta",
|
|
177
|
+
"secondaryCta",
|
|
178
|
+
"disclaimer",
|
|
179
|
+
"imageLinks",
|
|
180
|
+
].forEach(field => {
|
|
181
|
+
expect(
|
|
182
|
+
container.querySelector(`[data-contentful-field-id="${field}"]`)
|
|
183
|
+
).toHaveAttribute("data-contentful-entry-id", "image-promo");
|
|
184
|
+
});
|
|
185
|
+
expect(
|
|
186
|
+
container.querySelector(
|
|
187
|
+
'[data-contentful-entry-id="image-link-item"][data-contentful-field-id="image"]'
|
|
188
|
+
)
|
|
189
|
+
).toBeInTheDocument();
|
|
190
|
+
expect(screen.getAllByTestId("cta-button")[0]).toHaveAttribute(
|
|
191
|
+
"data-contentful-field-id",
|
|
192
|
+
"nestedPrimaryCta"
|
|
193
|
+
);
|
|
194
|
+
expect(screen.getAllByTestId("cta-button")[1]).toHaveAttribute(
|
|
195
|
+
"data-contentful-field-id",
|
|
196
|
+
"nestedSecondaryCta"
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("applies block CTA tags to wrappers around custom check-plans content", () => {
|
|
201
|
+
const tag = (field: string) => ({
|
|
202
|
+
"data-contentful-field-id": field,
|
|
203
|
+
"data-contentful-entry-id": "image-promo",
|
|
204
|
+
});
|
|
205
|
+
const { container } = render(
|
|
206
|
+
<ImagePromoBar
|
|
207
|
+
{...defaultProps}
|
|
208
|
+
cta={{
|
|
209
|
+
preDefinedFunctionExecution: "check availability",
|
|
210
|
+
fieldProps: { "data-contentful-field-id": "primaryButton" },
|
|
211
|
+
}}
|
|
212
|
+
secondaryCta={{
|
|
213
|
+
preDefinedFunctionExecution: "check availability",
|
|
214
|
+
fieldProps: { "data-contentful-field-id": "secondaryButton" },
|
|
215
|
+
}}
|
|
216
|
+
renderCheckPlans={() => <button>Check availability</button>}
|
|
217
|
+
fieldProps={{
|
|
218
|
+
cta: tag("cta"),
|
|
219
|
+
secondaryCta: tag("secondaryCta"),
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const primaryWrapper = container.querySelector(".primary-cta");
|
|
225
|
+
const secondaryWrapper = container.querySelector(".secondary-cta");
|
|
226
|
+
expect(primaryWrapper).toHaveAttribute("data-contentful-field-id", "cta");
|
|
227
|
+
expect(secondaryWrapper).toHaveAttribute(
|
|
228
|
+
"data-contentful-field-id",
|
|
229
|
+
"secondaryCta"
|
|
230
|
+
);
|
|
231
|
+
expect(primaryWrapper).toHaveAttribute(
|
|
232
|
+
"data-contentful-entry-id",
|
|
233
|
+
"image-promo"
|
|
234
|
+
);
|
|
235
|
+
expect(secondaryWrapper).toHaveAttribute(
|
|
236
|
+
"data-contentful-entry-id",
|
|
237
|
+
"image-promo"
|
|
238
|
+
);
|
|
239
|
+
expect(primaryWrapper?.querySelector("button")).toHaveTextContent(
|
|
240
|
+
"Check availability"
|
|
241
|
+
);
|
|
242
|
+
expect(secondaryWrapper?.querySelector("button")).toHaveTextContent(
|
|
243
|
+
"Check availability"
|
|
244
|
+
);
|
|
245
|
+
expect(primaryWrapper?.querySelector("button")).not.toHaveAttribute(
|
|
246
|
+
"data-contentful-field-id",
|
|
247
|
+
"primaryButton"
|
|
248
|
+
);
|
|
249
|
+
expect(secondaryWrapper?.querySelector("button")).not.toHaveAttribute(
|
|
250
|
+
"data-contentful-field-id",
|
|
251
|
+
"secondaryButton"
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
97
255
|
describe("Text content", () => {
|
|
98
256
|
it("renders brow text when provided", () => {
|
|
99
257
|
render(<ImagePromoBar {...defaultProps} brow="New" />);
|
|
@@ -37,6 +37,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
37
37
|
onVideoEvent,
|
|
38
38
|
renderCheckPlans,
|
|
39
39
|
enableAnimation = false,
|
|
40
|
+
fieldProps,
|
|
40
41
|
}) => {
|
|
41
42
|
const hasVideoLink = Boolean(videoLink);
|
|
42
43
|
|
|
@@ -56,6 +57,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
56
57
|
<Text
|
|
57
58
|
as="div"
|
|
58
59
|
className="subheading4 mb-4 text-text-brand xl:subheading2 xl:mb-3"
|
|
60
|
+
{...fieldProps?.brow}
|
|
59
61
|
>
|
|
60
62
|
{brow}
|
|
61
63
|
</Text>
|
|
@@ -64,6 +66,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
64
66
|
<Text
|
|
65
67
|
as={enableHeading ? "h1" : "h2"}
|
|
66
68
|
className="heading2 xl:heading1"
|
|
69
|
+
{...fieldProps?.title}
|
|
67
70
|
>
|
|
68
71
|
{title}
|
|
69
72
|
</Text>
|
|
@@ -72,6 +75,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
72
75
|
<Text
|
|
73
76
|
as={enableHeading ? "h2" : "h3"}
|
|
74
77
|
className="subheading3 mt-3 md:subheading1"
|
|
78
|
+
{...fieldProps?.subTitle}
|
|
75
79
|
>
|
|
76
80
|
{subTitle}
|
|
77
81
|
</Text>
|
|
@@ -79,45 +83,54 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
79
83
|
</div>
|
|
80
84
|
{/* Content Section */}
|
|
81
85
|
{description && (
|
|
82
|
-
<Text as="div" className="body1">
|
|
86
|
+
<Text as="div" className="body1" {...fieldProps?.description}>
|
|
83
87
|
{description}
|
|
84
88
|
</Text>
|
|
85
89
|
)}
|
|
86
90
|
{/* Checklist Rendering */}
|
|
87
91
|
{checklist.length > 0 && (
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
<div {...fieldProps?.checklist}>
|
|
93
|
+
<Checklist
|
|
94
|
+
items={checklist}
|
|
95
|
+
iconPosition="top"
|
|
96
|
+
iconSize={24}
|
|
97
|
+
listItemClassName="body1 text-text"
|
|
98
|
+
listContainerClassName="mt-0 space-y-0 flex flex-col gap-3"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
{ctaDisclaimer && (
|
|
103
|
+
<div {...fieldProps?.ctaDisclaimer}>{ctaDisclaimer}</div>
|
|
95
104
|
)}
|
|
96
|
-
{ctaDisclaimer && <div>{ctaDisclaimer}</div>}
|
|
97
105
|
{imageLinks.length > 0 && (
|
|
98
|
-
<div className="flex gap-4">
|
|
106
|
+
<div className="flex gap-4" {...fieldProps?.imageLinks}>
|
|
99
107
|
{/* Image Links Collection */}
|
|
100
|
-
{imageLinks?.map(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
{imageLinks?.map((link, index: number) => (
|
|
109
|
+
<div
|
|
110
|
+
key={index}
|
|
111
|
+
className="image-link w-[147px]"
|
|
112
|
+
{...link.fieldProps?.image}
|
|
113
|
+
>
|
|
114
|
+
<Link
|
|
115
|
+
variant="unstyled"
|
|
116
|
+
href={link.url}
|
|
117
|
+
target="_blank"
|
|
118
|
+
rel="noopener noreferrer"
|
|
119
|
+
>
|
|
120
|
+
<Image src={link.image} alt="icon-link" />
|
|
121
|
+
</Link>
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
114
124
|
</div>
|
|
115
125
|
)}
|
|
116
126
|
{/* CTAs and Disclaimers */}
|
|
117
127
|
{(cta || secondaryCta) && (
|
|
118
128
|
<div className="flex w-full flex-col gap-4 xl:flex-row">
|
|
119
129
|
{cta && (
|
|
120
|
-
<div
|
|
130
|
+
<div
|
|
131
|
+
className="primary-cta w-full xl:w-auto xl:shrink-0"
|
|
132
|
+
{...fieldProps?.cta}
|
|
133
|
+
>
|
|
121
134
|
<Button
|
|
122
135
|
{...cta}
|
|
123
136
|
// Block-level `enableAnimation` takes precedence:
|
|
@@ -134,11 +147,15 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
134
147
|
renderCheckPlans={renderCheckPlans}
|
|
135
148
|
onModalButtonClick={onModalButtonClick}
|
|
136
149
|
onClick={(e: React.MouseEvent) => onClick?.(e, "primary")}
|
|
150
|
+
fieldProps={cta.fieldProps}
|
|
137
151
|
/>
|
|
138
152
|
</div>
|
|
139
153
|
)}
|
|
140
154
|
{secondaryCta && (
|
|
141
|
-
<div
|
|
155
|
+
<div
|
|
156
|
+
className="secondary-cta w-full xl:w-auto xl:shrink-0"
|
|
157
|
+
{...fieldProps?.secondaryCta}
|
|
158
|
+
>
|
|
142
159
|
<Button
|
|
143
160
|
{...secondaryCta}
|
|
144
161
|
// Block-level `enableAnimation` takes precedence:
|
|
@@ -158,6 +175,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
158
175
|
onClick={(e: React.MouseEvent) =>
|
|
159
176
|
onClick?.(e, "secondary")
|
|
160
177
|
}
|
|
178
|
+
fieldProps={secondaryCta.fieldProps}
|
|
161
179
|
/>
|
|
162
180
|
</div>
|
|
163
181
|
)}
|
|
@@ -209,7 +227,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
209
227
|
|
|
210
228
|
{disclaimer && (
|
|
211
229
|
<div className="mt-10 flex justify-center xl:mt-18">
|
|
212
|
-
<div>{disclaimer}</div>
|
|
230
|
+
<div {...fieldProps?.disclaimer}>{disclaimer}</div>
|
|
213
231
|
</div>
|
|
214
232
|
)}
|
|
215
233
|
</div>
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
VideoAnalyticsHandler,
|
|
5
5
|
VideoLinkProps,
|
|
6
6
|
} from "@shared/components/video-link";
|
|
7
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
7
8
|
import { CheckPlansProps } from "@shared/types/micro-components";
|
|
8
9
|
|
|
9
10
|
export type ImagePromoBarProps = {
|
|
@@ -22,7 +23,11 @@ export type ImagePromoBarProps = {
|
|
|
22
23
|
openDescriptionLinksOnANewTab: boolean;
|
|
23
24
|
checklist: string[];
|
|
24
25
|
disclaimer: React.ReactNode;
|
|
25
|
-
imageLinks: {
|
|
26
|
+
imageLinks: {
|
|
27
|
+
url: string;
|
|
28
|
+
image: string;
|
|
29
|
+
fieldProps?: FieldProps<"image">;
|
|
30
|
+
}[];
|
|
26
31
|
cta: any;
|
|
27
32
|
secondaryCta: any;
|
|
28
33
|
ctaDisclaimer: React.ReactNode;
|
|
@@ -34,4 +39,17 @@ export type ImagePromoBarProps = {
|
|
|
34
39
|
onModalButtonClick?: (id?: string) => void;
|
|
35
40
|
onClick?: (event: React.MouseEvent, source: "primary" | "secondary") => void;
|
|
36
41
|
renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
|
|
42
|
+
/** Consumer-supplied DOM attributes for the rendered content fields. */
|
|
43
|
+
fieldProps?: FieldProps<
|
|
44
|
+
| "brow"
|
|
45
|
+
| "title"
|
|
46
|
+
| "subTitle"
|
|
47
|
+
| "description"
|
|
48
|
+
| "checklist"
|
|
49
|
+
| "ctaDisclaimer"
|
|
50
|
+
| "cta"
|
|
51
|
+
| "secondaryCta"
|
|
52
|
+
| "disclaimer"
|
|
53
|
+
| "imageLinks"
|
|
54
|
+
>;
|
|
37
55
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { PrimaryHero } from "./index";
|
|
3
3
|
|
|
4
|
+
import { replaceTokens } from "@shared/utils/replace-tokens";
|
|
4
5
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
5
6
|
|
|
6
7
|
jest.mock("@shared/components/checklist", () => ({
|
|
@@ -26,8 +27,10 @@ jest.mock("@shared/components/next-image", () => ({
|
|
|
26
27
|
}));
|
|
27
28
|
|
|
28
29
|
jest.mock("@shared/components/text", () => ({
|
|
29
|
-
Text: ({ as: Tag = "span", children, className }: any) => (
|
|
30
|
-
<Tag className={className}
|
|
30
|
+
Text: ({ as: Tag = "span", children, className, ...rest }: any) => (
|
|
31
|
+
<Tag className={className} {...rest}>
|
|
32
|
+
{children}
|
|
33
|
+
</Tag>
|
|
31
34
|
),
|
|
32
35
|
}));
|
|
33
36
|
|
|
@@ -80,10 +83,25 @@ describe("PrimaryHero", () => {
|
|
|
80
83
|
});
|
|
81
84
|
|
|
82
85
|
describe("Price section", () => {
|
|
86
|
+
const getPriceLockup = (container: HTMLElement) => {
|
|
87
|
+
const amount = container.querySelector("p.subheading5");
|
|
88
|
+
const lockup = amount?.parentElement;
|
|
89
|
+
const [dollarSign, , suffix] = Array.from(lockup?.children ?? []);
|
|
90
|
+
return { lockup, dollarSign, amount, suffix };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const textExposedToAssistiveTech = (container: HTMLElement) => {
|
|
94
|
+
const clone = container.cloneNode(true) as HTMLElement;
|
|
95
|
+
clone
|
|
96
|
+
.querySelectorAll('[aria-hidden="true"]')
|
|
97
|
+
.forEach(node => node.remove());
|
|
98
|
+
return clone.textContent ?? "";
|
|
99
|
+
};
|
|
100
|
+
|
|
83
101
|
it("renders price with dollar sign", () => {
|
|
84
|
-
render(<PrimaryHero {...baseProps} price="49
|
|
102
|
+
render(<PrimaryHero {...baseProps} price="49" />);
|
|
85
103
|
expect(screen.getByText("$")).toBeInTheDocument();
|
|
86
|
-
expect(screen.getByText("49
|
|
104
|
+
expect(screen.getByText("49")).toBeInTheDocument();
|
|
87
105
|
});
|
|
88
106
|
|
|
89
107
|
it("renders priceSuffix when provided", () => {
|
|
@@ -117,6 +135,247 @@ describe("PrimaryHero", () => {
|
|
|
117
135
|
render(<PrimaryHero {...baseProps} />);
|
|
118
136
|
expect(screen.queryByText("$")).not.toBeInTheDocument();
|
|
119
137
|
});
|
|
138
|
+
|
|
139
|
+
it("keeps the whole-dollar authored lockup unchanged", () => {
|
|
140
|
+
const { container } = render(
|
|
141
|
+
<PrimaryHero {...baseProps} price="19" priceSuffix="99/mo*" />
|
|
142
|
+
);
|
|
143
|
+
const { lockup, dollarSign, amount, suffix } = getPriceLockup(container);
|
|
144
|
+
|
|
145
|
+
expect(lockup?.children).toHaveLength(3);
|
|
146
|
+
expect(dollarSign).toHaveTextContent(/^\$$/);
|
|
147
|
+
expect(dollarSign).toHaveClass("subheading1", "mt-0.5");
|
|
148
|
+
expect(amount).toHaveTextContent(/^19$/);
|
|
149
|
+
expect(suffix?.tagName).toBe("P");
|
|
150
|
+
expect(suffix).toHaveClass("subheading1", "mt-0.5");
|
|
151
|
+
expect(suffix).toHaveTextContent(/^99\/mo\*$/);
|
|
152
|
+
expect(suffix?.querySelector("span")).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("renders decimal cents with the suffix so it matches the authored lockup", () => {
|
|
156
|
+
const authored = render(
|
|
157
|
+
<PrimaryHero {...baseProps} price="19" priceSuffix="99/mo*" />
|
|
158
|
+
);
|
|
159
|
+
const expected = getPriceLockup(authored.container);
|
|
160
|
+
const expectedLockup = {
|
|
161
|
+
amount: expected.amount?.textContent,
|
|
162
|
+
amountClass: expected.amount?.className,
|
|
163
|
+
suffix: expected.suffix?.textContent,
|
|
164
|
+
suffixClass: expected.suffix?.className,
|
|
165
|
+
count: expected.lockup?.children.length,
|
|
166
|
+
};
|
|
167
|
+
authored.unmount();
|
|
168
|
+
|
|
169
|
+
const { container } = render(
|
|
170
|
+
<PrimaryHero {...baseProps} price="19.99" priceSuffix="/mo*" />
|
|
171
|
+
);
|
|
172
|
+
const actual = getPriceLockup(container);
|
|
173
|
+
|
|
174
|
+
expect({
|
|
175
|
+
amount: actual.amount?.textContent,
|
|
176
|
+
amountClass: actual.amount?.className,
|
|
177
|
+
suffix: actual.suffix?.textContent,
|
|
178
|
+
suffixClass: actual.suffix?.className,
|
|
179
|
+
count: actual.lockup?.children.length,
|
|
180
|
+
}).toEqual(expectedLockup);
|
|
181
|
+
expect(screen.queryByText("19.99")).not.toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("renders a resolved price token like the authored production lockup", () => {
|
|
185
|
+
const tokens = [
|
|
186
|
+
{ token: "price_fiber_300", value: "24.99", promo: "19.99" },
|
|
187
|
+
];
|
|
188
|
+
const { container } = render(
|
|
189
|
+
<PrimaryHero
|
|
190
|
+
{...baseProps}
|
|
191
|
+
price={replaceTokens("${price_fiber_300}", tokens)}
|
|
192
|
+
priceSuffix={replaceTokens("/mo*", tokens)}
|
|
193
|
+
priceCallout="fiber plans|start at "
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
const { amount, suffix } = getPriceLockup(container);
|
|
197
|
+
|
|
198
|
+
expect(amount).toHaveTextContent(/^19$/);
|
|
199
|
+
expect(suffix).toHaveTextContent(/^99\/mo\*$/);
|
|
200
|
+
expect(screen.getByText("fiber plans")).toBeInTheDocument();
|
|
201
|
+
expect(screen.getByText("$19.99/mo*")).toHaveClass("sr-only");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("renders decimal cents on their own when no suffix is provided", () => {
|
|
205
|
+
const { container } = render(
|
|
206
|
+
<PrimaryHero {...baseProps} price="39.99" />
|
|
207
|
+
);
|
|
208
|
+
const { amount, suffix } = getPriceLockup(container);
|
|
209
|
+
|
|
210
|
+
expect(amount).toHaveTextContent(/^39$/);
|
|
211
|
+
expect(suffix).toHaveClass("subheading1", "mt-0.5");
|
|
212
|
+
expect(suffix).toHaveTextContent(/^99$/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("splits prices with thousands separators", () => {
|
|
216
|
+
const { container } = render(
|
|
217
|
+
<PrimaryHero {...baseProps} price="1,299.50" priceSuffix="/yr" />
|
|
218
|
+
);
|
|
219
|
+
const { amount, suffix } = getPriceLockup(container);
|
|
220
|
+
|
|
221
|
+
expect(amount).toHaveTextContent(/^1,299$/);
|
|
222
|
+
expect(suffix).toHaveTextContent(/^50\/yr$/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it.each(["Free", "$19.99", "19.", ".99"])(
|
|
226
|
+
"renders non-decimal price %p verbatim",
|
|
227
|
+
price => {
|
|
228
|
+
const { container } = render(
|
|
229
|
+
<PrimaryHero {...baseProps} price={price} priceSuffix="/mo" />
|
|
230
|
+
);
|
|
231
|
+
const { amount, suffix } = getPriceLockup(container);
|
|
232
|
+
|
|
233
|
+
expect(amount?.textContent).toBe(price);
|
|
234
|
+
expect(suffix).toHaveTextContent(/^\/mo$/);
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
it.each([
|
|
239
|
+
["price", { price: "${price_tier3_2gig}" }],
|
|
240
|
+
["priceSuffix", { price: "19", priceSuffix: "${price_suffix}" }],
|
|
241
|
+
["priceCallout", { price: "19", priceCallout: "save $${reward_x}" }],
|
|
242
|
+
])("hides the price lockup when %s has an unresolved token", (_, props) => {
|
|
243
|
+
const { container } = render(
|
|
244
|
+
<PrimaryHero
|
|
245
|
+
{...baseProps}
|
|
246
|
+
priceSuffix="/mo*"
|
|
247
|
+
priceCallout="fiber plans|start at "
|
|
248
|
+
{...props}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(container.querySelector("p.subheading5")).toBeNull();
|
|
253
|
+
expect(screen.queryByText("$")).not.toBeInTheDocument();
|
|
254
|
+
expect(screen.queryByText("fiber plans")).not.toBeInTheDocument();
|
|
255
|
+
expect(container.textContent).not.toContain("${");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it.each(["99/mo*", " 99/mo*", "99"])(
|
|
259
|
+
"does not repeat cents already leading the suffix %p",
|
|
260
|
+
priceSuffix => {
|
|
261
|
+
const { container } = render(
|
|
262
|
+
<PrimaryHero {...baseProps} price="59.99" priceSuffix={priceSuffix} />
|
|
263
|
+
);
|
|
264
|
+
const { amount, suffix } = getPriceLockup(container);
|
|
265
|
+
|
|
266
|
+
expect(amount).toHaveTextContent(/^59$/);
|
|
267
|
+
expect(suffix?.textContent).toBe(priceSuffix);
|
|
268
|
+
expect(suffix?.querySelector("span")).toBeNull();
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
it.each([
|
|
273
|
+
["59.49", "99/mo*", "4999/mo*"],
|
|
274
|
+
["19.99", "990/mo", "99990/mo"],
|
|
275
|
+
])(
|
|
276
|
+
"keeps the cents when %p has a suffix %p that does not lead with them",
|
|
277
|
+
(price, priceSuffix, expected) => {
|
|
278
|
+
const { container } = render(
|
|
279
|
+
<PrimaryHero {...baseProps} price={price} priceSuffix={priceSuffix} />
|
|
280
|
+
);
|
|
281
|
+
const { suffix } = getPriceLockup(container);
|
|
282
|
+
|
|
283
|
+
expect(suffix?.textContent).toBe(expected);
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
it("tags split cents with the price field and the suffix with the priceSuffix field", () => {
|
|
288
|
+
const { container } = render(
|
|
289
|
+
<PrimaryHero
|
|
290
|
+
{...baseProps}
|
|
291
|
+
price="19.99"
|
|
292
|
+
priceSuffix="/mo*"
|
|
293
|
+
fieldProps={{
|
|
294
|
+
price: { "data-field": "price" },
|
|
295
|
+
priceSuffix: { "data-field": "priceSuffix" },
|
|
296
|
+
}}
|
|
297
|
+
/>
|
|
298
|
+
);
|
|
299
|
+
const { amount, suffix } = getPriceLockup(container);
|
|
300
|
+
const cents = suffix?.querySelector("span");
|
|
301
|
+
|
|
302
|
+
expect(amount).toHaveAttribute("data-field", "price");
|
|
303
|
+
expect(suffix).toHaveAttribute("data-field", "priceSuffix");
|
|
304
|
+
expect(cents).toHaveAttribute("data-field", "price");
|
|
305
|
+
expect(cents).toHaveTextContent(/^99$/);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it.each([
|
|
309
|
+
["19.99", "/mo*", "$19.99/mo*"],
|
|
310
|
+
["59.99", "99/mo*", "$59.99/mo*"],
|
|
311
|
+
["59.99", " 99/mo*", "$59.99/mo*"],
|
|
312
|
+
["39.99", undefined, "$39.99"],
|
|
313
|
+
["1,299.50", "/yr", "$1,299.50/yr"],
|
|
314
|
+
["19.99", " per month", "$19.99 per month"],
|
|
315
|
+
["59.49", "99/mo*", "$59.49 99/mo*"],
|
|
316
|
+
])(
|
|
317
|
+
"exposes %p + %p to assistive technology as %p only once",
|
|
318
|
+
(price, priceSuffix, expected) => {
|
|
319
|
+
const { container } = render(
|
|
320
|
+
<PrimaryHero
|
|
321
|
+
{...baseProps}
|
|
322
|
+
price={price}
|
|
323
|
+
priceSuffix={priceSuffix}
|
|
324
|
+
priceCallout="fiber plans|start at "
|
|
325
|
+
/>
|
|
326
|
+
);
|
|
327
|
+
const { lockup } = getPriceLockup(container);
|
|
328
|
+
const accessibleText = textExposedToAssistiveTech(container);
|
|
329
|
+
|
|
330
|
+
expect(screen.getByText(expected)).toHaveClass("sr-only");
|
|
331
|
+
expect(lockup).toHaveAttribute("aria-hidden", "true");
|
|
332
|
+
expect(accessibleText).toContain(expected);
|
|
333
|
+
expect(accessibleText.replace(expected, "")).not.toMatch(/\d/);
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
it("keeps the split visual fragments in the DOM for crawlers", () => {
|
|
338
|
+
const { container } = render(
|
|
339
|
+
<PrimaryHero {...baseProps} price="19.99" priceSuffix="/mo*" />
|
|
340
|
+
);
|
|
341
|
+
const { dollarSign, amount, suffix } = getPriceLockup(container);
|
|
342
|
+
|
|
343
|
+
expect(dollarSign).toBeInTheDocument();
|
|
344
|
+
expect(amount).toHaveTextContent(/^19$/);
|
|
345
|
+
expect(suffix).toHaveTextContent(/^99\/mo\*$/);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it.each([
|
|
349
|
+
["19", "99/mo*"],
|
|
350
|
+
["Free", "/mo"],
|
|
351
|
+
["19.", "/mo"],
|
|
352
|
+
])(
|
|
353
|
+
"leaves the unsplit lockup %p + %p exposed to assistive technology as before",
|
|
354
|
+
(price, priceSuffix) => {
|
|
355
|
+
const { container } = render(
|
|
356
|
+
<PrimaryHero {...baseProps} price={price} priceSuffix={priceSuffix} />
|
|
357
|
+
);
|
|
358
|
+
const { lockup } = getPriceLockup(container);
|
|
359
|
+
|
|
360
|
+
expect(container.querySelector(".sr-only")).toBeNull();
|
|
361
|
+
expect(lockup).not.toHaveAttribute("aria-hidden");
|
|
362
|
+
expect(textExposedToAssistiveTech(container)).toContain(
|
|
363
|
+
`$${price}${priceSuffix}`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
it("does not expose an accessible price when the lockup is hidden", () => {
|
|
369
|
+
const { container } = render(
|
|
370
|
+
<PrimaryHero
|
|
371
|
+
{...baseProps}
|
|
372
|
+
price="${price_tier3_2gig}"
|
|
373
|
+
priceSuffix="/mo*"
|
|
374
|
+
/>
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
expect(container.querySelector(".sr-only")).toBeNull();
|
|
378
|
+
});
|
|
120
379
|
});
|
|
121
380
|
|
|
122
381
|
describe("Checklist", () => {
|