@windstream/react-shared-components 0.2.92 → 0.2.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contentful/index.d.ts +46 -9
- package/dist/contentful/index.esm.js +4 -4
- 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 +6 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.esm.js +2 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +2 -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 +1 -1
- package/src/components/checklist/index.test.tsx +18 -1
- package/src/components/checklist/index.tsx +25 -21
- package/src/components/checklist/types.ts +8 -2
- package/src/contentful/blocks/address-input-banner/index.test.tsx +14 -0
- package/src/contentful/blocks/address-input-banner/index.tsx +3 -1
- package/src/contentful/blocks/address-input-banner/types.ts +1 -1
- package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +91 -0
- package/src/contentful/blocks/anchored-bottom-banner/index.tsx +18 -11
- package/src/contentful/blocks/anchored-bottom-banner/types.ts +17 -3
- package/src/contentful/blocks/blogs-grid/index.test.tsx +21 -1
- package/src/contentful/blocks/blogs-grid/index.tsx +1 -0
- package/src/contentful/blocks/blogs-grid/types.ts +3 -0
- package/src/contentful/blocks/blogs-grid-base/index.test.tsx +49 -3
- package/src/contentful/blocks/blogs-grid-base/index.tsx +11 -2
- package/src/contentful/blocks/blogs-grid-base/types.ts +7 -1
- package/src/contentful/blocks/cards/blog-card/index.test.tsx +29 -0
- package/src/contentful/blocks/cards/blog-card/index.tsx +18 -4
- package/src/contentful/blocks/cards/blog-card/types.ts +3 -1
- package/src/contentful/blocks/cards/product-card/index.test.tsx +43 -0
- package/src/contentful/blocks/cards/product-card/index.tsx +37 -9
- package/src/contentful/blocks/cards/product-card/types.ts +3 -0
- package/src/contentful/blocks/carousel/helper.test.tsx +13 -0
- package/src/contentful/blocks/carousel/helper.tsx +1 -0
- package/src/contentful/blocks/carousel/types.ts +2 -0
- package/src/contentful/blocks/dynamic-form/conditional-fields.tsx +1 -0
- package/src/contentful/blocks/dynamic-form/form-field.tsx +1 -0
- package/src/contentful/blocks/dynamic-form/index.test.tsx +48 -0
- package/src/contentful/blocks/dynamic-form/index.tsx +19 -7
- package/src/contentful/blocks/dynamic-form/render.tsx +13 -7
- package/src/contentful/blocks/dynamic-form/types.ts +28 -2
- package/src/contentful/blocks/dynamic-tabs/index.test.tsx +52 -2
- package/src/contentful/blocks/dynamic-tabs/index.tsx +9 -2
- package/src/contentful/blocks/dynamic-tabs/ordered-tab-views.tsx +16 -6
- package/src/contentful/blocks/dynamic-tabs/tab-views.tsx +10 -6
- package/src/contentful/blocks/dynamic-tabs/types.ts +6 -0
- package/src/contentful/blocks/email-input-block/index.test.tsx +51 -3
- package/src/contentful/blocks/email-input-block/index.tsx +88 -31
- package/src/contentful/blocks/email-input-block/types.ts +14 -1
- package/src/contentful/blocks/fiber-form/index.test.tsx +40 -2
- package/src/contentful/blocks/fiber-form/index.tsx +32 -13
- package/src/contentful/blocks/fiber-form/types.ts +18 -0
- package/src/contentful/blocks/iframe-embed/index.test.tsx +44 -7
- package/src/contentful/blocks/iframe-embed/index.tsx +3 -2
- package/src/contentful/blocks/iframe-embed/types.ts +1 -0
- package/src/contentful/blocks/navigation/{Navigation.stories.tsx → navigation.stories.tsx} +1 -1
- package/src/contentful/blocks/ordered-list/index.test.tsx +54 -4
- package/src/contentful/blocks/ordered-list/index.tsx +14 -2
- package/src/contentful/blocks/ordered-list/types.ts +15 -2
- package/src/contentful/blocks/primary-hero/index.tsx +5 -5
- package/src/contentful/blocks/search-block/index.test.tsx +36 -2
- package/src/contentful/blocks/search-block/index.tsx +24 -4
- package/src/contentful/blocks/search-block/types.ts +10 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/replace-tokens.test.ts +40 -0
- package/src/utils/replace-tokens.ts +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useEffect, useRef } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { bgColorMap, IframeEmbedProps } from "./types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* IframeEmbed — A generic, responsive iframe component for embedding third-party content
|
|
@@ -30,6 +30,7 @@ export const IframeEmbed: React.FC<IframeEmbedProps> = ({
|
|
|
30
30
|
anchorId,
|
|
31
31
|
backgroundColor,
|
|
32
32
|
height,
|
|
33
|
+
fieldProps,
|
|
33
34
|
}) => {
|
|
34
35
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
35
36
|
|
|
@@ -56,7 +57,7 @@ export const IframeEmbed: React.FC<IframeEmbedProps> = ({
|
|
|
56
57
|
if (!iframeUrl) return null;
|
|
57
58
|
|
|
58
59
|
return (
|
|
59
|
-
<section id={anchorId} className="w-full">
|
|
60
|
+
<section id={anchorId} className="w-full" {...fieldProps?.iframeUrl}>
|
|
60
61
|
<iframe
|
|
61
62
|
ref={iframeRef}
|
|
62
63
|
src={iframeUrl}
|
|
@@ -6,16 +6,24 @@ import { OrderedList } from "./index";
|
|
|
6
6
|
import { render, screen, within } from "@testing-library/react";
|
|
7
7
|
|
|
8
8
|
jest.mock("@shared/components/link", () => ({
|
|
9
|
-
Link: ({ children, href, title, className }: any) => (
|
|
10
|
-
<a
|
|
9
|
+
Link: ({ children, href, title, className, ...rest }: any) => (
|
|
10
|
+
<a
|
|
11
|
+
href={href}
|
|
12
|
+
title={title}
|
|
13
|
+
className={className}
|
|
14
|
+
data-testid="ol-link"
|
|
15
|
+
{...rest}
|
|
16
|
+
>
|
|
11
17
|
{children}
|
|
12
18
|
</a>
|
|
13
19
|
),
|
|
14
20
|
}));
|
|
15
21
|
|
|
16
22
|
jest.mock("@shared/components/text", () => ({
|
|
17
|
-
Text: ({ children, as: Tag = "span", className }: any) => (
|
|
18
|
-
<Tag className={className}
|
|
23
|
+
Text: ({ children, as: Tag = "span", className, ...rest }: any) => (
|
|
24
|
+
<Tag className={className} {...rest}>
|
|
25
|
+
{children}
|
|
26
|
+
</Tag>
|
|
19
27
|
),
|
|
20
28
|
}));
|
|
21
29
|
|
|
@@ -83,6 +91,48 @@ describe("OrderedList", () => {
|
|
|
83
91
|
expect(screen.getByText("Find availability near you")).toBeInTheDocument();
|
|
84
92
|
});
|
|
85
93
|
|
|
94
|
+
it("tags headings and child names and links independently", () => {
|
|
95
|
+
renderComponent({
|
|
96
|
+
fieldProps: {
|
|
97
|
+
title: { "data-contentful-field-id": "title" },
|
|
98
|
+
description: { "data-contentful-field-id": "description" },
|
|
99
|
+
},
|
|
100
|
+
tabs: [
|
|
101
|
+
{
|
|
102
|
+
items: {
|
|
103
|
+
tabName: "Cities",
|
|
104
|
+
tabContent: [
|
|
105
|
+
{
|
|
106
|
+
itemName: "new york",
|
|
107
|
+
itemSlug: "/ny",
|
|
108
|
+
fieldProps: {
|
|
109
|
+
itemName: { "data-contentful-field-id": "cityName" },
|
|
110
|
+
itemSlug: { "data-contentful-field-id": "slug" },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
expect(screen.getByText("Browse locations")).toHaveAttribute(
|
|
119
|
+
"data-contentful-field-id",
|
|
120
|
+
"title"
|
|
121
|
+
);
|
|
122
|
+
expect(screen.getByText("Find availability near you")).toHaveAttribute(
|
|
123
|
+
"data-contentful-field-id",
|
|
124
|
+
"description"
|
|
125
|
+
);
|
|
126
|
+
expect(screen.getByText("New York")).toHaveAttribute(
|
|
127
|
+
"data-contentful-field-id",
|
|
128
|
+
"cityName"
|
|
129
|
+
);
|
|
130
|
+
expect(screen.getByTestId("ol-link")).toHaveAttribute(
|
|
131
|
+
"data-contentful-field-id",
|
|
132
|
+
"slug"
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
86
136
|
it("renders the section with the provided anchor id and testid", () => {
|
|
87
137
|
const { container } = renderComponent({ anchorId: "locations" });
|
|
88
138
|
const section = container.querySelector(
|
|
@@ -13,6 +13,7 @@ export const OrderedList = ({
|
|
|
13
13
|
title,
|
|
14
14
|
description,
|
|
15
15
|
tabs,
|
|
16
|
+
fieldProps,
|
|
16
17
|
}: OrderedListProps) => {
|
|
17
18
|
const formatItemName = (name: string) =>
|
|
18
19
|
name
|
|
@@ -31,6 +32,7 @@ export const OrderedList = ({
|
|
|
31
32
|
<Text
|
|
32
33
|
key={`ordered-item-${index}`}
|
|
33
34
|
className="body1 relative text-left font-semibold leading-7"
|
|
35
|
+
{...item.fieldProps?.itemName}
|
|
34
36
|
>
|
|
35
37
|
{formatItemName(item.itemName)}
|
|
36
38
|
</Text>
|
|
@@ -41,10 +43,14 @@ export const OrderedList = ({
|
|
|
41
43
|
<Link
|
|
42
44
|
key={`ordered-item-${index}`}
|
|
43
45
|
href={item.itemSlug}
|
|
46
|
+
{...item.fieldProps?.itemSlug}
|
|
44
47
|
title={item.itemName.toUpperCase()}
|
|
45
48
|
className="flex items-center underline underline-offset-4"
|
|
46
49
|
>
|
|
47
|
-
<Text
|
|
50
|
+
<Text
|
|
51
|
+
className="body1 relative cursor-pointer text-left font-semibold leading-7"
|
|
52
|
+
{...item.fieldProps?.itemName}
|
|
53
|
+
>
|
|
48
54
|
{formatItemName(item.itemName)}
|
|
49
55
|
</Text>
|
|
50
56
|
</Link>
|
|
@@ -56,6 +62,7 @@ export const OrderedList = ({
|
|
|
56
62
|
const dynamicTabs = {
|
|
57
63
|
items: tabs.map((tab, index) => ({
|
|
58
64
|
tabName: tab.items.tabName,
|
|
65
|
+
fieldProps: tab.items.fieldProps,
|
|
59
66
|
anchorId: `tab-${index}`,
|
|
60
67
|
tabContent: tab.items.tabContent.map(renderChild),
|
|
61
68
|
})),
|
|
@@ -69,12 +76,17 @@ export const OrderedList = ({
|
|
|
69
76
|
>
|
|
70
77
|
<div className="mx-auto w-full max-w-120 px-0 py-12 md:px-6 md:py-20 lg:px-3">
|
|
71
78
|
<div className="mb-11 px-4 md:px-0">
|
|
72
|
-
<Text
|
|
79
|
+
<Text
|
|
80
|
+
as={"h2"}
|
|
81
|
+
className="heading2 text-center md:heading1"
|
|
82
|
+
{...fieldProps?.title}
|
|
83
|
+
>
|
|
73
84
|
{title}
|
|
74
85
|
</Text>
|
|
75
86
|
<Text
|
|
76
87
|
as={"h3"}
|
|
77
88
|
className="subheading3 mt-2 text-center md:subheading1"
|
|
89
|
+
{...fieldProps?.description}
|
|
78
90
|
>
|
|
79
91
|
{description}
|
|
80
92
|
</Text>
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
4
|
+
|
|
3
5
|
export interface OrderedListProps {
|
|
4
6
|
anchorId?: string;
|
|
5
7
|
backgroundColor?: "white" | "cream";
|
|
6
8
|
title: ReactNode;
|
|
7
9
|
description: ReactNode;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
fieldProps?: FieldProps<"title" | "description">;
|
|
11
|
+
tabs: {
|
|
12
|
+
items: {
|
|
13
|
+
tabName: string;
|
|
14
|
+
fieldProps?: FieldProps<"tabName">;
|
|
15
|
+
tabContent: {
|
|
16
|
+
itemName: string;
|
|
17
|
+
itemSlug?: string;
|
|
18
|
+
fieldProps?: FieldProps<"itemName" | "itemSlug">;
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
21
|
+
}[];
|
|
22
|
+
}
|
|
@@ -324,7 +324,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
324
324
|
"check availability" && (
|
|
325
325
|
<Text
|
|
326
326
|
as="span"
|
|
327
|
-
className="
|
|
327
|
+
className="body3 mr-1"
|
|
328
328
|
{...fieldProps?.secondaryCtaPrefix}
|
|
329
329
|
>
|
|
330
330
|
{secondaryCtaPrefix}
|
|
@@ -333,7 +333,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
333
333
|
{secondaryCta && (
|
|
334
334
|
<Button
|
|
335
335
|
{...secondaryCta}
|
|
336
|
-
linkClassName={`
|
|
336
|
+
linkClassName={`body3 ${textColorClass}`}
|
|
337
337
|
renderCheckPlans={renderCheckPlans}
|
|
338
338
|
onModalButtonClick={onModalButtonClick}
|
|
339
339
|
/>
|
|
@@ -346,7 +346,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
346
346
|
<div className="hidden lg:block">
|
|
347
347
|
<Button
|
|
348
348
|
{...bottomLink}
|
|
349
|
-
linkClassName={`
|
|
349
|
+
linkClassName={`body3 ${textColorClass}`}
|
|
350
350
|
renderCheckPlans={renderCheckPlans}
|
|
351
351
|
onModalButtonClick={onModalButtonClick}
|
|
352
352
|
/>
|
|
@@ -423,7 +423,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
423
423
|
!secondaryCta?.preDefinedFunctionExecution && (
|
|
424
424
|
<Text
|
|
425
425
|
as="span"
|
|
426
|
-
className="
|
|
426
|
+
className="body3 mr-1"
|
|
427
427
|
{...fieldProps?.secondaryCtaPrefix}
|
|
428
428
|
>
|
|
429
429
|
{secondaryCtaPrefix}
|
|
@@ -434,7 +434,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
434
434
|
{...secondaryCta}
|
|
435
435
|
renderCheckPlans={renderCheckPlans}
|
|
436
436
|
onModalButtonClick={onModalButtonClick}
|
|
437
|
-
linkClassName={
|
|
437
|
+
linkClassName={`body3 ${textColorClass}`}
|
|
438
438
|
/>
|
|
439
439
|
)}
|
|
440
440
|
</div>
|
|
@@ -43,8 +43,10 @@ jest.mock("@shared/components/material-icon", () => ({
|
|
|
43
43
|
}));
|
|
44
44
|
|
|
45
45
|
jest.mock("@shared/components/text", () => ({
|
|
46
|
-
Text: ({ as: Tag = "span", children, className }: any) => (
|
|
47
|
-
<Tag className={className}
|
|
46
|
+
Text: ({ as: Tag = "span", children, className, ...rest }: any) => (
|
|
47
|
+
<Tag className={className} {...rest}>
|
|
48
|
+
{children}
|
|
49
|
+
</Tag>
|
|
48
50
|
),
|
|
49
51
|
}));
|
|
50
52
|
|
|
@@ -59,6 +61,38 @@ const defaultProps: SearchBlockProps = {
|
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
describe("SearchBlock", () => {
|
|
64
|
+
it("tags each authored field on its own element", () => {
|
|
65
|
+
const tag = (field: string) => ({ "data-contentful-field-id": field });
|
|
66
|
+
const { container } = render(
|
|
67
|
+
<SearchBlock
|
|
68
|
+
{...defaultProps}
|
|
69
|
+
subTitle="Subtitle"
|
|
70
|
+
searchDescription="Description"
|
|
71
|
+
caption={<p>Caption</p>}
|
|
72
|
+
basicCaption={<p>Basic</p>}
|
|
73
|
+
fieldProps={{
|
|
74
|
+
title: tag("title"),
|
|
75
|
+
subTitle: tag("subTitle"),
|
|
76
|
+
searchDescription: tag("description"),
|
|
77
|
+
searchInputPlaceholder: tag("searchInputPlaceholder"),
|
|
78
|
+
caption: tag("caption"),
|
|
79
|
+
basicCaption: tag("basicCaption"),
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
for (const field of [
|
|
84
|
+
"title",
|
|
85
|
+
"subTitle",
|
|
86
|
+
"description",
|
|
87
|
+
"searchInputPlaceholder",
|
|
88
|
+
"caption",
|
|
89
|
+
"basicCaption",
|
|
90
|
+
]) {
|
|
91
|
+
expect(
|
|
92
|
+
container.querySelector(`[data-contentful-field-id="${field}"]`)
|
|
93
|
+
).not.toBeNull();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
62
96
|
describe("Rendering", () => {
|
|
63
97
|
it("renders with data-testid", () => {
|
|
64
98
|
render(<SearchBlock {...defaultProps} />);
|
|
@@ -20,6 +20,7 @@ export const SearchBlock: React.FC<SearchBlockProps> = props => {
|
|
|
20
20
|
backgroundColor,
|
|
21
21
|
color,
|
|
22
22
|
onSubmit,
|
|
23
|
+
fieldProps,
|
|
23
24
|
} = props;
|
|
24
25
|
const [searchText, setSearchText] = useState("");
|
|
25
26
|
const themeColorClasses = {
|
|
@@ -45,17 +46,27 @@ export const SearchBlock: React.FC<SearchBlockProps> = props => {
|
|
|
45
46
|
<Text
|
|
46
47
|
as={enableHeading ? "h1" : "h2"}
|
|
47
48
|
className={cx("heading2 md:heading1")}
|
|
49
|
+
{...fieldProps?.title}
|
|
48
50
|
>
|
|
49
51
|
{title}
|
|
50
52
|
</Text>
|
|
51
53
|
{subTitle && (
|
|
52
|
-
<Text
|
|
54
|
+
<Text
|
|
55
|
+
as="div"
|
|
56
|
+
className={cx("subheading2 mt-2 md:subheading1")}
|
|
57
|
+
{...fieldProps?.subTitle}
|
|
58
|
+
>
|
|
53
59
|
{subTitle}
|
|
54
60
|
</Text>
|
|
55
61
|
)}
|
|
56
62
|
</div>
|
|
57
63
|
{searchDescription ? (
|
|
58
|
-
<Text
|
|
64
|
+
<Text
|
|
65
|
+
className={`heading6 text-center`}
|
|
66
|
+
{...fieldProps?.searchDescription}
|
|
67
|
+
>
|
|
68
|
+
{searchDescription}
|
|
69
|
+
</Text>
|
|
59
70
|
) : null}
|
|
60
71
|
|
|
61
72
|
<span className="w-full">
|
|
@@ -73,14 +84,23 @@ export const SearchBlock: React.FC<SearchBlockProps> = props => {
|
|
|
73
84
|
target: { value: React.SetStateAction<string> };
|
|
74
85
|
}) => setSearchText(e.target.value)}
|
|
75
86
|
placeholder={searchInputPlaceholder}
|
|
87
|
+
{...fieldProps?.searchInputPlaceholder}
|
|
76
88
|
containerClassName="px-4 pl-0 rounded-md flex-grow border-none"
|
|
77
89
|
className={"body3 rounded-md text-text"}
|
|
78
90
|
/>
|
|
79
91
|
</div>
|
|
80
92
|
</form>
|
|
81
93
|
</span>
|
|
82
|
-
{caption
|
|
83
|
-
|
|
94
|
+
{caption && fieldProps?.caption ? (
|
|
95
|
+
<div {...fieldProps.caption}>{caption}</div>
|
|
96
|
+
) : (
|
|
97
|
+
caption || null
|
|
98
|
+
)}
|
|
99
|
+
{basicCaption && fieldProps?.basicCaption ? (
|
|
100
|
+
<div {...fieldProps.basicCaption}>{basicCaption}</div>
|
|
101
|
+
) : (
|
|
102
|
+
basicCaption
|
|
103
|
+
)}
|
|
84
104
|
</div>
|
|
85
105
|
</div>
|
|
86
106
|
</div>
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
4
|
+
|
|
3
5
|
export type SearchBlockProps = {
|
|
6
|
+
fieldProps?: FieldProps<
|
|
7
|
+
| "title"
|
|
8
|
+
| "subTitle"
|
|
9
|
+
| "searchDescription"
|
|
10
|
+
| "searchInputPlaceholder"
|
|
11
|
+
| "caption"
|
|
12
|
+
| "basicCaption"
|
|
13
|
+
>;
|
|
4
14
|
anchorId?: string;
|
|
5
15
|
title?: string;
|
|
6
16
|
subTitle?: string;
|
package/src/utils/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ export {
|
|
|
34
34
|
|
|
35
35
|
// Token replacement — framework-agnostic string / rich-text interpolation.
|
|
36
36
|
// No dependency on Contentful or any particular CMS/tool.
|
|
37
|
-
export { replaceTokens } from "./replace-tokens";
|
|
37
|
+
export { replaceTokens, getTokenField } from "./replace-tokens";
|
|
38
38
|
export type {
|
|
39
39
|
TokenEntry,
|
|
40
40
|
TokenSource,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
getTokenField,
|
|
2
3
|
hasUnresolvedTokens,
|
|
3
4
|
replaceTokens,
|
|
4
5
|
type RichTextDocument,
|
|
@@ -229,3 +230,42 @@ describe("hasUnresolvedTokens", () => {
|
|
|
229
230
|
]);
|
|
230
231
|
});
|
|
231
232
|
});
|
|
233
|
+
|
|
234
|
+
describe("getTokenField", () => {
|
|
235
|
+
it("trims the token key before matching", () => {
|
|
236
|
+
expect(getTokenField(" price_tier1_300mbps ", tokens, "value")).toBe(
|
|
237
|
+
"44.99"
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("parses a JSON string token source", () => {
|
|
242
|
+
expect(
|
|
243
|
+
getTokenField("price_tier1_300mbps", JSON.stringify(tokens), "value")
|
|
244
|
+
).toBe("44.99");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("returns value (strikethrough) and promo (plan price) separately", () => {
|
|
248
|
+
expect(getTokenField("price_tier1_300mbps", tokens, "value")).toBe("44.99");
|
|
249
|
+
expect(getTokenField("price_tier1_300mbps", tokens, "promo")).toBe("39.99");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("returns undefined for a missing field, unknown token or empty key", () => {
|
|
253
|
+
expect(getTokenField("price_value_only", tokens, "promo")).toBeUndefined();
|
|
254
|
+
expect(getTokenField("price_unknown", tokens, "value")).toBeUndefined();
|
|
255
|
+
expect(getTokenField("", tokens, "value")).toBeUndefined();
|
|
256
|
+
expect(getTokenField(undefined, tokens, "value")).toBeUndefined();
|
|
257
|
+
expect(getTokenField("price_tier1_300mbps", null, "value")).toBeUndefined();
|
|
258
|
+
expect(
|
|
259
|
+
getTokenField("price_tier1_300mbps", "{bad", "value")
|
|
260
|
+
).toBeUndefined();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("ignores blank or non-string field values", () => {
|
|
264
|
+
const source = [
|
|
265
|
+
{ token: "blank", value: " " },
|
|
266
|
+
{ token: "numeric", value: 44 as unknown as string },
|
|
267
|
+
];
|
|
268
|
+
expect(getTokenField("blank", source, "value")).toBeUndefined();
|
|
269
|
+
expect(getTokenField("numeric", source, "value")).toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -44,6 +44,19 @@ const buildTokenMap = (tokens: TokenSource): Record<string, string> =>
|
|
|
44
44
|
return acc;
|
|
45
45
|
}, {});
|
|
46
46
|
|
|
47
|
+
export const getTokenField = (
|
|
48
|
+
token: string | null | undefined,
|
|
49
|
+
tokens: TokenSource,
|
|
50
|
+
field: "promo" | "value"
|
|
51
|
+
): string | undefined => {
|
|
52
|
+
if (typeof token !== "string" || !token.trim()) return undefined;
|
|
53
|
+
const key = token.trim();
|
|
54
|
+
const resolved = normalizeEntries(tokens).find(
|
|
55
|
+
entry => entry?.token === key
|
|
56
|
+
)?.[field];
|
|
57
|
+
return typeof resolved === "string" && resolved.trim() ? resolved : undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
47
60
|
const replaceInString = (input: string, map: Record<string, string>): string =>
|
|
48
61
|
input.replace(TOKEN_PATTERN, (match, token) =>
|
|
49
62
|
Object.prototype.hasOwnProperty.call(map, token) ? map[token] : match
|