@windstream/react-shared-components 0.1.93 → 0.1.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contentful/index.esm.js +2 -2
- 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 +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.js +1 -1
- package/package.json +14 -8
- package/src/components/accordion/index.test.tsx +270 -0
- package/src/components/alert-card/index.test.tsx +152 -0
- package/src/components/animation-wrapper/index.test.tsx +424 -0
- package/src/components/brand-button/index.test.tsx +292 -0
- package/src/components/button/index.test.tsx +91 -0
- package/src/components/call-button/index.test.tsx +260 -0
- package/src/components/checkbox/index.test.tsx +252 -0
- package/src/components/checklist/index.test.tsx +231 -0
- package/src/components/checklist/index.tsx +64 -29
- package/src/components/checklist/types.ts +7 -1
- package/src/components/collapse/index.test.tsx +277 -0
- package/src/components/collapse/index.tsx +1 -0
- package/src/components/divider/index.test.tsx +53 -0
- package/src/components/image/index.test.tsx +174 -0
- package/src/components/input/index.test.tsx +348 -0
- package/src/components/link/index.test.tsx +199 -0
- package/src/components/list/index.test.tsx +166 -0
- package/src/components/material-icon/index.test.tsx +130 -0
- package/src/components/modal/index.test.tsx +310 -0
- package/src/components/next-image/index.test.tsx +406 -0
- package/src/components/pagination/index.test.tsx +521 -0
- package/src/components/radio-button/index.test.tsx +151 -0
- package/src/components/see-more/index.test.tsx +96 -0
- package/src/components/select/index.test.tsx +256 -0
- package/src/components/select-plan-button/index.test.tsx +173 -0
- package/src/components/skeleton/index.test.tsx +74 -0
- package/src/components/spinner/index.test.tsx +76 -0
- package/src/components/text/index.test.tsx +65 -0
- package/src/components/tooltip/index.test.tsx +50 -0
- package/src/components/view-cart-button/index.test.tsx +57 -0
- package/src/contentful/blocks/accordion/index.test.tsx +218 -0
- package/src/contentful/blocks/accordion/index.tsx +3 -1
- package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -0
- package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -0
- package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +5 -4
- package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -0
- package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -0
- package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -0
- package/src/contentful/blocks/button/index.test.tsx +339 -0
- package/src/contentful/blocks/callout/index.test.tsx +539 -0
- package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -0
- package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -0
- package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -0
- package/src/contentful/blocks/cards/index.test.tsx +39 -0
- package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -0
- package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -0
- package/src/contentful/blocks/cards/simple-card/index.tsx +1 -1
- package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -0
- package/src/contentful/blocks/carousel/helper.test.tsx +539 -0
- package/src/contentful/blocks/carousel/index.test.tsx +308 -0
- package/src/contentful/blocks/carousel/types.test.ts +16 -0
- package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -0
- package/src/contentful/blocks/cart-retention-banner/index.tsx +4 -4
- package/src/contentful/blocks/comparison-table/index.test.tsx +114 -0
- package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -0
- package/src/contentful/blocks/cta-callout/index.test.tsx +244 -0
- package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -0
- package/src/contentful/blocks/email-input-block/index.test.tsx +213 -0
- package/src/contentful/blocks/email-input-block/index.tsx +40 -35
- package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -0
- package/src/contentful/blocks/floating-banner/index.test.tsx +246 -0
- package/src/contentful/blocks/footer/index.test.tsx +302 -0
- package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -0
- package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -0
- package/src/contentful/blocks/image-promo-bar/index.tsx +248 -246
- package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -0
- package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -0
- package/src/contentful/blocks/modal/index.test.tsx +209 -0
- package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -0
- package/src/contentful/blocks/navigation/index.test.tsx +924 -0
- package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -0
- package/src/contentful/blocks/primary-hero/index.test.tsx +286 -0
- package/src/contentful/blocks/primary-hero/index.tsx +7 -4
- package/src/contentful/blocks/search-block/index.test.tsx +268 -0
- package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -0
- package/src/contentful/blocks/text/index.test.tsx +36 -0
- package/src/contentful/index.test.ts +45 -0
- package/src/global-mocks/contentful/to-document.ts +25 -0
- package/src/global-mocks/cookie.ts +48 -0
- package/src/global-mocks/cx.ts +37 -0
- package/src/global-mocks/index.ts +89 -0
- package/src/global-mocks/speed-card-bg.ts +27 -0
- package/src/global-mocks/utm.ts +49 -0
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -0
- package/src/hooks/contentful/use-contentful-rich-text.tsx +1 -1
- package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -0
- package/src/hooks/use-body-scroll-lock.test.ts +134 -0
- package/src/hooks/use-carousel-swipe.test.ts +393 -0
- package/src/hooks/use-outside-click.test.ts +142 -0
- package/src/index.ts +1 -1
- package/src/next/index.test.ts +7 -0
- package/src/setupTests.ts +17 -11
- package/src/utils/contentful/to-document.test.ts +85 -0
- package/src/utils/cookie.test.ts +180 -0
- package/src/utils/cx.test.ts +90 -0
- package/src/utils/index.test.ts +115 -0
- package/src/utils/speed-card-bg.test.ts +46 -0
- package/src/utils/utm.test.ts +359 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DynamicTabs } from "./index";
|
|
3
|
+
|
|
4
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
5
|
+
|
|
6
|
+
jest.mock("framer-motion", () => ({
|
|
7
|
+
AnimatePresence: ({ children }: any) => <>{children}</>,
|
|
8
|
+
motion: {
|
|
9
|
+
div: ({ children, animate }: any) => (
|
|
10
|
+
<div data-testid="motion-div" style={{ opacity: animate?.opacity }}>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
jest.mock("@shared/components/collapse", () => ({
|
|
18
|
+
Collapse: ({ open, children }: any) => (
|
|
19
|
+
<div data-testid="collapse" data-open={open}>
|
|
20
|
+
{open ? children : null}
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
jest.mock("@shared/components/material-icon", () => ({
|
|
26
|
+
MaterialIcon: ({ name }: any) => (
|
|
27
|
+
<span data-testid="material-icon">{name}</span>
|
|
28
|
+
),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock("@shared/components/text", () => ({
|
|
32
|
+
Text: ({ children, className }: any) => (
|
|
33
|
+
<span className={className}>{children}</span>
|
|
34
|
+
),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
jest.mock("@shared/contentful/blocks/button", () => ({
|
|
38
|
+
Button: ({ children, onClick, buttonClassName }: any) => (
|
|
39
|
+
<button
|
|
40
|
+
data-testid="tab-button"
|
|
41
|
+
onClick={onClick}
|
|
42
|
+
className={buttonClassName}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
</button>
|
|
46
|
+
),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
jest.mock("@shared/utils", () => ({
|
|
50
|
+
cx: (...args: any[]) => args.filter(Boolean).join(" "),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
describe("DynamicTabs", () => {
|
|
54
|
+
const defaultProps = {
|
|
55
|
+
tabs: {
|
|
56
|
+
items: [
|
|
57
|
+
{ tabName: "Tab 1", anchorId: "tab-1", tabContent: { items: [] } },
|
|
58
|
+
{ tabName: "Tab 2", anchorId: "tab-2", tabContent: { items: [] } },
|
|
59
|
+
{ tabName: "Tab 3", anchorId: "tab-3", tabContent: { items: [] } },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
tabsContent: [
|
|
63
|
+
<div key="1">Content 1</div>,
|
|
64
|
+
<div key="2">Content 2</div>,
|
|
65
|
+
<div key="3">Content 3</div>,
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
jest.clearAllMocks();
|
|
71
|
+
window.location.hash = "";
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("Rendering", () => {
|
|
75
|
+
it("renders all tab buttons", () => {
|
|
76
|
+
render(<DynamicTabs {...defaultProps} />);
|
|
77
|
+
expect(screen.getAllByText("Tab 1").length).toBeGreaterThan(0);
|
|
78
|
+
expect(screen.getAllByText("Tab 2").length).toBeGreaterThan(0);
|
|
79
|
+
expect(screen.getAllByText("Tab 3").length).toBeGreaterThan(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("renders tab content", () => {
|
|
83
|
+
render(<DynamicTabs {...defaultProps} />);
|
|
84
|
+
expect(screen.getAllByText("Content 1").length).toBeGreaterThan(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("renders title when provided", () => {
|
|
88
|
+
render(<DynamicTabs {...defaultProps} title="My Tabs" />);
|
|
89
|
+
expect(screen.getByText("My Tabs")).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders subtitle when provided", () => {
|
|
93
|
+
render(<DynamicTabs {...defaultProps} subTitle="A subtitle" />);
|
|
94
|
+
expect(screen.getByText("A subtitle")).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not render title section when no title or subtitle", () => {
|
|
98
|
+
const { container } = render(<DynamicTabs {...defaultProps} />);
|
|
99
|
+
expect(container.querySelector(".mb-8.mt-5")).not.toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("Tab switching", () => {
|
|
104
|
+
it("switches tab content on click", () => {
|
|
105
|
+
render(<DynamicTabs {...defaultProps} />);
|
|
106
|
+
const tabButtons = screen.getAllByTestId("tab-button");
|
|
107
|
+
// Find a Tab 2 button and click it
|
|
108
|
+
const tab2Buttons = tabButtons.filter(btn => btn.textContent === "Tab 2");
|
|
109
|
+
fireEvent.click(tab2Buttons[0]);
|
|
110
|
+
// After clicking Tab 2, the motion-div for tab 2 should show opacity 1
|
|
111
|
+
const motionDivs = screen.getAllByTestId("motion-div");
|
|
112
|
+
expect(motionDivs.length).toBeGreaterThan(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("first tab is active by default", () => {
|
|
116
|
+
render(<DynamicTabs {...defaultProps} />);
|
|
117
|
+
const tabButtons = screen.getAllByTestId("tab-button");
|
|
118
|
+
const firstTabBtn = tabButtons.find(btn => btn.textContent === "Tab 1");
|
|
119
|
+
expect(firstTabBtn?.className).toContain("border-b-2");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("Navigation type", () => {
|
|
124
|
+
it("renders tab view on desktop when navigationType is true", () => {
|
|
125
|
+
const { container } = render(
|
|
126
|
+
<DynamicTabs {...defaultProps} navigationType={true} />
|
|
127
|
+
);
|
|
128
|
+
// Desktop view should have the overflow-x-auto div in "hidden md:block"
|
|
129
|
+
const desktopBlock = container.querySelector(".hidden.md\\:block");
|
|
130
|
+
expect(desktopBlock).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("renders sidebar view on desktop when navigationType is false", () => {
|
|
134
|
+
const { container } = render(
|
|
135
|
+
<DynamicTabs {...defaultProps} navigationType={false} />
|
|
136
|
+
);
|
|
137
|
+
const desktopBlock = container.querySelector(".hidden.md\\:block");
|
|
138
|
+
expect(desktopBlock).toBeInTheDocument();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("Mobile view", () => {
|
|
143
|
+
it("renders tab view on mobile when mobileSidebarTabType is true", () => {
|
|
144
|
+
const { container } = render(
|
|
145
|
+
<DynamicTabs {...defaultProps} mobileSidebarTabType={true} />
|
|
146
|
+
);
|
|
147
|
+
const mobileBlock = container.querySelector(".md\\:hidden");
|
|
148
|
+
expect(mobileBlock).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("renders accordion view on mobile when mobileSidebarTabType is false", () => {
|
|
152
|
+
render(<DynamicTabs {...defaultProps} mobileSidebarTabType={false} />);
|
|
153
|
+
// Accordion uses Collapse component
|
|
154
|
+
const collapses = screen.getAllByTestId("collapse");
|
|
155
|
+
expect(collapses.length).toBeGreaterThan(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("clicks accordion button to switch tabs", () => {
|
|
159
|
+
window.location.hash = "#tab-1";
|
|
160
|
+
const { container } = render(
|
|
161
|
+
<DynamicTabs {...defaultProps} mobileSidebarTabType={false} />
|
|
162
|
+
);
|
|
163
|
+
// Accordion buttons are inside the mobile container (.md\:hidden)
|
|
164
|
+
const mobileContainer = container.querySelector(".md\\:hidden");
|
|
165
|
+
const accordionButtons = mobileContainer!.querySelectorAll(
|
|
166
|
+
"[data-testid='tab-button']"
|
|
167
|
+
);
|
|
168
|
+
// Click the second accordion button (Tab 2)
|
|
169
|
+
fireEvent.click(accordionButtons[1]);
|
|
170
|
+
expect(window.location.hash).toBe("#tab-2");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("Sidebar view", () => {
|
|
175
|
+
it("clicks sidebar button to switch tabs", () => {
|
|
176
|
+
window.location.hash = "#tab-1";
|
|
177
|
+
const { container } = render(
|
|
178
|
+
<DynamicTabs {...defaultProps} navigationType={false} />
|
|
179
|
+
);
|
|
180
|
+
// Sidebar buttons are inside the desktop container (.hidden.md\:block)
|
|
181
|
+
const desktopContainer = container.querySelector(".hidden.md\\:block");
|
|
182
|
+
const sidebarButtons = desktopContainer!.querySelectorAll(
|
|
183
|
+
"[data-testid='tab-button']"
|
|
184
|
+
);
|
|
185
|
+
// Click the third sidebar button (Tab 3)
|
|
186
|
+
fireEvent.click(sidebarButtons[2]);
|
|
187
|
+
expect(window.location.hash).toBe("#tab-3");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("Hash navigation", () => {
|
|
192
|
+
it("sets tab based on URL hash", () => {
|
|
193
|
+
window.location.hash = "#tab-2";
|
|
194
|
+
render(<DynamicTabs {...defaultProps} />);
|
|
195
|
+
// After useEffect runs, tab 2 should be active
|
|
196
|
+
const tabButtons = screen.getAllByTestId("tab-button");
|
|
197
|
+
const tab2Btns = tabButtons.filter(btn => btn.textContent === "Tab 2");
|
|
198
|
+
// At least one Tab 2 button should have the active style
|
|
199
|
+
const hasActiveTab2 = tab2Btns.some(btn =>
|
|
200
|
+
btn.className.includes("border-b-2")
|
|
201
|
+
);
|
|
202
|
+
expect(hasActiveTab2).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("updates hash on tab click when fragment exists", () => {
|
|
206
|
+
window.location.hash = "#tab-1";
|
|
207
|
+
render(<DynamicTabs {...defaultProps} />);
|
|
208
|
+
const tabButtons = screen.getAllByTestId("tab-button");
|
|
209
|
+
const tab2Btns = tabButtons.filter(btn => btn.textContent === "Tab 2");
|
|
210
|
+
fireEvent.click(tab2Btns[0]);
|
|
211
|
+
expect(window.location.hash).toBe("#tab-2");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("listens to hashchange events", () => {
|
|
215
|
+
render(<DynamicTabs {...defaultProps} />);
|
|
216
|
+
act(() => {
|
|
217
|
+
window.location.hash = "#tab-3";
|
|
218
|
+
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
|
219
|
+
});
|
|
220
|
+
const tabButtons = screen.getAllByTestId("tab-button");
|
|
221
|
+
const tab3Btns = tabButtons.filter(btn => btn.textContent === "Tab 3");
|
|
222
|
+
const hasActiveTab3 = tab3Btns.some(btn =>
|
|
223
|
+
btn.className.includes("border-b-2")
|
|
224
|
+
);
|
|
225
|
+
expect(hasActiveTab3).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("Empty state", () => {
|
|
230
|
+
it("renders without tabs", () => {
|
|
231
|
+
const { container } = render(<DynamicTabs />);
|
|
232
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("renders without tabsContent", () => {
|
|
236
|
+
render(<DynamicTabs tabs={defaultProps.tabs} />);
|
|
237
|
+
expect(screen.getAllByTestId("tab-button").length).toBeGreaterThan(0);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { EmailInputBlock } from "./index";
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
describe("EmailInputBlock", () => {
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
onSubmit: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("renders with default props", () => {
|
|
15
|
+
const { container } = render(<EmailInputBlock {...defaultProps} />);
|
|
16
|
+
expect(container.querySelector("#email-input")).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("renders custom anchorId", () => {
|
|
20
|
+
const { container } = render(
|
|
21
|
+
<EmailInputBlock {...defaultProps} anchorId="custom-anchor" />
|
|
22
|
+
);
|
|
23
|
+
expect(container.querySelector("#custom-anchor")).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders title when provided", () => {
|
|
27
|
+
render(<EmailInputBlock {...defaultProps} title="Join us" />);
|
|
28
|
+
expect(screen.getByText("Join us")).toBeInTheDocument();
|
|
29
|
+
expect(screen.getByText("Join us").tagName).toBe("H1");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("does not render title when not provided", () => {
|
|
33
|
+
const { container } = render(<EmailInputBlock {...defaultProps} />);
|
|
34
|
+
expect(container.querySelector("h1")).not.toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders subTitle when provided", () => {
|
|
38
|
+
render(<EmailInputBlock {...defaultProps} subTitle="Stay updated" />);
|
|
39
|
+
expect(screen.getByText("Stay updated")).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText("Stay updated").tagName).toBe("H2");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not render subTitle when not provided", () => {
|
|
44
|
+
const { container } = render(<EmailInputBlock {...defaultProps} />);
|
|
45
|
+
expect(container.querySelector("h2")).not.toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders emailInputDescription when provided", () => {
|
|
49
|
+
render(
|
|
50
|
+
<EmailInputBlock {...defaultProps} emailInputDescription="Sign up now" />
|
|
51
|
+
);
|
|
52
|
+
expect(screen.getByText("Sign up now")).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders caption when provided", () => {
|
|
56
|
+
render(<EmailInputBlock {...defaultProps} caption="No spam" />);
|
|
57
|
+
expect(screen.getByText("No spam")).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("does not render caption when not provided", () => {
|
|
61
|
+
render(<EmailInputBlock {...defaultProps} />);
|
|
62
|
+
expect(screen.queryByText("No spam")).not.toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("uses default placeholder text", () => {
|
|
66
|
+
render(<EmailInputBlock {...defaultProps} />);
|
|
67
|
+
expect(
|
|
68
|
+
screen.getByPlaceholderText("Enter your email here")
|
|
69
|
+
).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("uses custom placeholder text", () => {
|
|
73
|
+
render(<EmailInputBlock {...defaultProps} inputPlaceholder="Your email" />);
|
|
74
|
+
expect(screen.getByPlaceholderText("Your email")).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("calls onSubmit with email on valid submission", () => {
|
|
78
|
+
render(<EmailInputBlock {...defaultProps} />);
|
|
79
|
+
const input = screen.getByPlaceholderText("Enter your email here");
|
|
80
|
+
fireEvent.change(input, { target: { value: "test@example.com" } });
|
|
81
|
+
fireEvent.click(screen.getByText("Sign me up"));
|
|
82
|
+
expect(defaultProps.onSubmit).toHaveBeenCalledWith({
|
|
83
|
+
email: "test@example.com",
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("clears email after successful submission", () => {
|
|
88
|
+
render(<EmailInputBlock {...defaultProps} />);
|
|
89
|
+
const input = screen.getByPlaceholderText(
|
|
90
|
+
"Enter your email here"
|
|
91
|
+
) as HTMLInputElement;
|
|
92
|
+
fireEvent.change(input, { target: { value: "test@example.com" } });
|
|
93
|
+
fireEvent.click(screen.getByText("Sign me up"));
|
|
94
|
+
expect(input.value).toBe("");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("shows error for invalid email", () => {
|
|
98
|
+
render(<EmailInputBlock {...defaultProps} />);
|
|
99
|
+
const input = screen.getByPlaceholderText("Enter your email here");
|
|
100
|
+
fireEvent.change(input, { target: { value: "invalid" } });
|
|
101
|
+
fireEvent.click(screen.getByText("Sign me up"));
|
|
102
|
+
expect(
|
|
103
|
+
screen.getByText("Please enter a valid email address.")
|
|
104
|
+
).toBeInTheDocument();
|
|
105
|
+
expect(defaultProps.onSubmit).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("clears error when user types after error", () => {
|
|
109
|
+
render(<EmailInputBlock {...defaultProps} />);
|
|
110
|
+
const input = screen.getByPlaceholderText("Enter your email here");
|
|
111
|
+
fireEvent.change(input, { target: { value: "invalid" } });
|
|
112
|
+
fireEvent.click(screen.getByText("Sign me up"));
|
|
113
|
+
expect(
|
|
114
|
+
screen.getByText("Please enter a valid email address.")
|
|
115
|
+
).toBeInTheDocument();
|
|
116
|
+
|
|
117
|
+
fireEvent.change(input, { target: { value: "a" } });
|
|
118
|
+
expect(
|
|
119
|
+
screen.queryByText("Please enter a valid email address.")
|
|
120
|
+
).not.toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("shows success feedback after valid submission", () => {
|
|
124
|
+
render(<EmailInputBlock {...defaultProps} successFeedback="Thanks!" />);
|
|
125
|
+
const input = screen.getByPlaceholderText("Enter your email here");
|
|
126
|
+
fireEvent.change(input, { target: { value: "user@test.com" } });
|
|
127
|
+
fireEvent.click(screen.getByText("Sign me up"));
|
|
128
|
+
expect(screen.getByText("Thanks!")).toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("hides success feedback when user types again", () => {
|
|
132
|
+
render(<EmailInputBlock {...defaultProps} successFeedback="Thanks!" />);
|
|
133
|
+
const input = screen.getByPlaceholderText("Enter your email here");
|
|
134
|
+
fireEvent.change(input, { target: { value: "user@test.com" } });
|
|
135
|
+
fireEvent.click(screen.getByText("Sign me up"));
|
|
136
|
+
expect(screen.getByText("Thanks!")).toBeInTheDocument();
|
|
137
|
+
|
|
138
|
+
fireEvent.change(input, { target: { value: "x" } });
|
|
139
|
+
expect(screen.queryByText("Thanks!")).not.toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("does not show success feedback without successFeedback prop", () => {
|
|
143
|
+
render(<EmailInputBlock {...defaultProps} />);
|
|
144
|
+
const input = screen.getByPlaceholderText("Enter your email here");
|
|
145
|
+
fireEvent.change(input, { target: { value: "user@test.com" } });
|
|
146
|
+
fireEvent.click(screen.getByText("Sign me up"));
|
|
147
|
+
// No success text rendered
|
|
148
|
+
expect(screen.queryByText("Thanks!")).not.toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("applies green background by default", () => {
|
|
152
|
+
const { container } = render(<EmailInputBlock {...defaultProps} />);
|
|
153
|
+
const inner = container.querySelector(".bg-bg-fill-success");
|
|
154
|
+
expect(inner).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("applies navy background class", () => {
|
|
158
|
+
const { container } = render(
|
|
159
|
+
<EmailInputBlock {...defaultProps} backgroundColor="navy" />
|
|
160
|
+
);
|
|
161
|
+
expect(container.querySelector(".bg-bg-fill-inverse")).toBeInTheDocument();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("applies blue background class", () => {
|
|
165
|
+
const { container } = render(
|
|
166
|
+
<EmailInputBlock {...defaultProps} backgroundColor="blue" />
|
|
167
|
+
);
|
|
168
|
+
expect(container.querySelector(".bg-bg-fill-brand")).toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("applies yellow background class", () => {
|
|
172
|
+
const { container } = render(
|
|
173
|
+
<EmailInputBlock {...defaultProps} backgroundColor="yellow" />
|
|
174
|
+
);
|
|
175
|
+
expect(
|
|
176
|
+
container.querySelector(".bg-bg-fill-brand-accent")
|
|
177
|
+
).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("applies white background class", () => {
|
|
181
|
+
const { container } = render(
|
|
182
|
+
<EmailInputBlock {...defaultProps} backgroundColor="white" />
|
|
183
|
+
);
|
|
184
|
+
expect(container.querySelector(".bg-white")).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("uses white text for green/blue/navy backgrounds", () => {
|
|
188
|
+
render(
|
|
189
|
+
<EmailInputBlock
|
|
190
|
+
{...defaultProps}
|
|
191
|
+
backgroundColor="navy"
|
|
192
|
+
title="Navy title"
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
expect(screen.getByText("Navy title").className).toContain("text-white");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("uses dark text for white/yellow backgrounds", () => {
|
|
199
|
+
render(
|
|
200
|
+
<EmailInputBlock
|
|
201
|
+
{...defaultProps}
|
|
202
|
+
backgroundColor="yellow"
|
|
203
|
+
title="Yellow title"
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
expect(screen.getByText("Yellow title").className).toContain("text-text");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("uses custom ctaText", () => {
|
|
210
|
+
render(<EmailInputBlock {...defaultProps} ctaText="Subscribe" />);
|
|
211
|
+
expect(screen.getByText("Subscribe")).toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -20,19 +20,18 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
20
20
|
|
|
21
21
|
const bgClasses: Record<string, string> = {
|
|
22
22
|
navy: "bg-bg-fill-inverse",
|
|
23
|
-
green: "bg-bg-fill-
|
|
23
|
+
green: "bg-bg-fill-brand",
|
|
24
24
|
blue: "bg-bg-fill-brand",
|
|
25
25
|
yellow: "bg-bg-fill-brand-accent",
|
|
26
|
-
white: "bg-
|
|
26
|
+
white: "bg-bg",
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const normalizedBg = (backgroundColor as string)?.toLowerCase?.() || "green";
|
|
30
|
+
const bgColorClass =
|
|
31
|
+
bgClasses[normalizedBg as keyof typeof bgClasses] || bgClasses.green;
|
|
31
32
|
|
|
32
33
|
// Determine if text should be white based on background
|
|
33
|
-
const textWhite = ["green", "blue", "navy"].includes(
|
|
34
|
-
backgroundColor as string
|
|
35
|
-
);
|
|
34
|
+
const textWhite = ["green", "blue", "navy"].includes(normalizedBg);
|
|
36
35
|
const textColor = textWhite ? "text-white" : "text-text";
|
|
37
36
|
|
|
38
37
|
const [email, setEmail] = useState("");
|
|
@@ -56,41 +55,47 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
56
55
|
};
|
|
57
56
|
|
|
58
57
|
return (
|
|
59
|
-
<section id={anchorId} className="w-full px-4
|
|
58
|
+
<section id={anchorId} className="w-full px-4 !pt-8 md:!pt-20 md:px-0">
|
|
60
59
|
<div
|
|
61
|
-
className={`mx-auto max-w-[1200px] overflow-hidden rounded-[
|
|
60
|
+
className={`mx-auto max-w-[1200px] overflow-hidden !rounded-[20px] md:rounded-[20px] flex flex-col items-start !px-5 !pt-6 !pb-10 md:!p-10 md:!pt-12 !h-[344px] md:!w-auto md:!h-[367px] ${bgColorClass}`}
|
|
61
|
+
data-testid="email-input-blocck"
|
|
62
62
|
>
|
|
63
|
-
<div className="
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
<div className="w-full">
|
|
64
|
+
<div className="lg:w-[548px]">
|
|
65
|
+
{title && (
|
|
66
|
+
<h1 className={`mb-3 text-[2.25rem] leading-[2.5rem] font-black md:text-heading1 md:font-black ${textColor}`}>{title}</h1>
|
|
67
|
+
)}
|
|
68
|
+
{subTitle && (
|
|
69
|
+
<h2 className={`mb-3 text-heading1 font-black ${textColor}`}>
|
|
70
|
+
{subTitle}
|
|
71
|
+
</h2>
|
|
72
|
+
)}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
{emailInputDescription && (
|
|
75
|
+
<p className={`!mb-4 md:!mb-16 text-xl leading-7 font-medium md:text-subheading1 md:!leading-8 md:!font-bold opacity-95 ${textColor}`}>
|
|
76
|
+
{emailInputDescription}
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
78
80
|
|
|
79
|
-
<div className="max-w-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
<div className="relative max-w-[548px] flex w-full flex-col items-center sm:flex-row sm:overflow-hidden sm:rounded-3xl sm:border-2 sm:!border-gray-600 sm:bg-white sm:p-1.5 sm:shadow-xl sm:transition-all md:!h-[75px] md:p-2">
|
|
82
|
+
<div className="w-full !rounded-lg border border-transparent bg-white p-1.5 shadow-md sm:border-0 sm:p-0 sm:rounded-none sm:shadow-none">
|
|
83
|
+
<input
|
|
84
|
+
type="email"
|
|
85
|
+
placeholder={inputPlaceholder}
|
|
86
|
+
value={email}
|
|
87
|
+
onChange={e => {
|
|
88
|
+
setEmail(e.target.value);
|
|
89
|
+
if (emailError) setEmailError("");
|
|
90
|
+
if (formSubmitted) setFormSubmitted(false);
|
|
91
|
+
}}
|
|
92
|
+
className="w-full flex-grow bg-transparent !pl-4 pr-6 py-3 text-[1rem] leading-none md:text-base font-normal text-text outline-none placeholder:input-text-placeholder md:py-4"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
91
95
|
<Button
|
|
92
96
|
buttonLabel={ctaText}
|
|
93
97
|
buttonVariant="primary_inverse"
|
|
98
|
+
buttonClassName="capitalize-first whitespace-nowrap rounded-2xl px-8 !text-lg mt-3 w-full sm:mt-0 sm:w-auto sm:absolute sm:right-[8px] sm:top-[calc(50%-28px)]"
|
|
94
99
|
onClick={handleOnSubmit}
|
|
95
100
|
/>
|
|
96
101
|
</div>
|