@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,252 @@
|
|
|
1
|
+
/// <reference types="@testing-library/jest-dom" />
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Checkbox } from "./index";
|
|
4
|
+
|
|
5
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
jest.mock("@shared/components/button", () => {
|
|
8
|
+
const MockButton = React.forwardRef<
|
|
9
|
+
HTMLButtonElement,
|
|
10
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
11
|
+
>(({ children, ...props }, ref) => (
|
|
12
|
+
<button ref={ref} {...props}>
|
|
13
|
+
{children}
|
|
14
|
+
</button>
|
|
15
|
+
));
|
|
16
|
+
MockButton.displayName = "MockButton";
|
|
17
|
+
return { Button: MockButton };
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
jest.mock("@shared/utils", () => ({
|
|
21
|
+
cx: (...args: unknown[]) => args.filter(Boolean).join(" "),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe("Checkbox", () => {
|
|
25
|
+
describe("rendering", () => {
|
|
26
|
+
it("renders an unchecked checkbox by default", () => {
|
|
27
|
+
render(<Checkbox name="test" />);
|
|
28
|
+
const input = screen.getByRole("checkbox");
|
|
29
|
+
expect(input).not.toBeChecked();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders a checked checkbox when checked is true", () => {
|
|
33
|
+
render(<Checkbox name="test" checked={true} />);
|
|
34
|
+
const input = screen.getByRole("checkbox");
|
|
35
|
+
expect(input).toBeChecked();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders with the correct name attribute", () => {
|
|
39
|
+
render(<Checkbox name="agreement" />);
|
|
40
|
+
const input = screen.getByRole("checkbox");
|
|
41
|
+
expect(input).toHaveAttribute("name", "agreement");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("renders with the correct value attribute", () => {
|
|
45
|
+
render(<Checkbox name="test" value="yes" />);
|
|
46
|
+
const input = screen.getByRole("checkbox");
|
|
47
|
+
expect(input).toHaveAttribute("value", "yes");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("applies displayName correctly", () => {
|
|
51
|
+
expect(Checkbox.displayName).toBe("Checkbox");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("label", () => {
|
|
56
|
+
it("renders a string label", () => {
|
|
57
|
+
render(<Checkbox name="test" label="Accept terms" />);
|
|
58
|
+
expect(screen.getByText("Accept terms")).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("renders a ReactNode label", () => {
|
|
62
|
+
render(
|
|
63
|
+
<Checkbox
|
|
64
|
+
name="test"
|
|
65
|
+
label={<span data-testid="custom-label">Custom</span>}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
expect(screen.getByTestId("custom-label")).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does not render label element when label is not provided", () => {
|
|
72
|
+
const { container } = render(<Checkbox name="test" />);
|
|
73
|
+
const labels = container.querySelectorAll("label");
|
|
74
|
+
// Only the icon label, no text label
|
|
75
|
+
expect(labels.length).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("applies labelClassName to the label", () => {
|
|
79
|
+
render(
|
|
80
|
+
<Checkbox name="test" label="Label" labelClassName="custom-label" />
|
|
81
|
+
);
|
|
82
|
+
const label = screen.getByText("Label");
|
|
83
|
+
expect(label.className).toContain("custom-label");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("applies error class to label when error is true", () => {
|
|
87
|
+
render(<Checkbox name="test" label="Label" error={true} />);
|
|
88
|
+
const label = screen.getByText("Label");
|
|
89
|
+
expect(label.className).toContain("text-text-critical");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("interactive behavior", () => {
|
|
94
|
+
it("calls onChange with boolean when handler expects a parameter", () => {
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
96
|
+
const handleChange = jest.fn((_isChecked: boolean) => undefined);
|
|
97
|
+
render(<Checkbox name="test" onChange={handleChange} />);
|
|
98
|
+
const input = screen.getByRole("checkbox");
|
|
99
|
+
fireEvent.click(input);
|
|
100
|
+
expect(handleChange).toHaveBeenCalledWith(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("calls onChange without parameters for zero-arity handler", () => {
|
|
104
|
+
const handleChange = jest.fn();
|
|
105
|
+
// Override length to simulate zero-arity
|
|
106
|
+
Object.defineProperty(handleChange, "length", { value: 0 });
|
|
107
|
+
render(<Checkbox name="test" onChange={handleChange} />);
|
|
108
|
+
const input = screen.getByRole("checkbox");
|
|
109
|
+
fireEvent.click(input);
|
|
110
|
+
expect(handleChange).toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does not call onChange when disabled", () => {
|
|
114
|
+
const handleChange = jest.fn();
|
|
115
|
+
render(<Checkbox name="test" onChange={handleChange} disabled={true} />);
|
|
116
|
+
const input = screen.getByRole("checkbox");
|
|
117
|
+
fireEvent.click(input);
|
|
118
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does not call onChange when state is disabled", () => {
|
|
122
|
+
const handleChange = jest.fn();
|
|
123
|
+
render(<Checkbox name="test" onChange={handleChange} state="disabled" />);
|
|
124
|
+
const input = screen.getByRole("checkbox");
|
|
125
|
+
fireEvent.click(input);
|
|
126
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("disabled state", () => {
|
|
131
|
+
it("sets disabled attribute when disabled prop is true", () => {
|
|
132
|
+
render(<Checkbox name="test" disabled={true} />);
|
|
133
|
+
const input = screen.getByRole("checkbox");
|
|
134
|
+
expect(input).toBeDisabled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("sets disabled attribute when state is disabled", () => {
|
|
138
|
+
render(<Checkbox name="test" state="disabled" />);
|
|
139
|
+
const input = screen.getByRole("checkbox");
|
|
140
|
+
expect(input).toBeDisabled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("applies disabled styling to unchecked icon", () => {
|
|
144
|
+
const { container } = render(<Checkbox name="test" disabled={true} />);
|
|
145
|
+
const rect = container.querySelector("rect");
|
|
146
|
+
expect(rect).toHaveClass("fill-checkbox-bg-surface-disabled");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("applies disabled styling to checked icon", () => {
|
|
150
|
+
const { container } = render(
|
|
151
|
+
<Checkbox name="test" disabled={true} checked={true} />
|
|
152
|
+
);
|
|
153
|
+
const rect = container.querySelector("rect");
|
|
154
|
+
expect(rect).toHaveClass("fill-checkbox-bg-surface-selected-disabled");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("focus state", () => {
|
|
159
|
+
it("applies focus outline class when state is focus", () => {
|
|
160
|
+
const { container } = render(<Checkbox name="test" state="focus" />);
|
|
161
|
+
const label = container.querySelector("label");
|
|
162
|
+
expect(label?.className).toContain("outline");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("required", () => {
|
|
167
|
+
it("sets required attribute when required is true", () => {
|
|
168
|
+
render(<Checkbox name="test" required={true} />);
|
|
169
|
+
const input = screen.getByRole("checkbox");
|
|
170
|
+
expect(input).toBeRequired();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("id handling", () => {
|
|
175
|
+
it("uses provided id for input and label", () => {
|
|
176
|
+
render(<Checkbox name="test" id="custom-id" label="Label" />);
|
|
177
|
+
const input = screen.getByRole("checkbox");
|
|
178
|
+
expect(input).toHaveAttribute("id", "custom-id");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("falls back to name when id is not provided", () => {
|
|
182
|
+
render(<Checkbox name="fallback-name" label="Label" />);
|
|
183
|
+
const input = screen.getByRole("checkbox");
|
|
184
|
+
expect(input).toHaveAttribute("id", "fallback-name");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("data-cy attribute", () => {
|
|
189
|
+
it("passes data-cy to the input element", () => {
|
|
190
|
+
render(<Checkbox name="test" data-cy="checkbox-cy" />);
|
|
191
|
+
const input = screen.getByRole("checkbox");
|
|
192
|
+
expect(input).toHaveAttribute("data-cy", "checkbox-cy");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("renderInfoIcon", () => {
|
|
197
|
+
it("renders info icon button when renderInfoIcon is provided", () => {
|
|
198
|
+
const onClick = jest.fn();
|
|
199
|
+
render(
|
|
200
|
+
<Checkbox
|
|
201
|
+
name="test"
|
|
202
|
+
renderInfoIcon={{ onClick, dataTestId: "info-btn" }}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
expect(screen.getByTestId("info-btn")).toBeInTheDocument();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("calls onClick when info icon is clicked", () => {
|
|
209
|
+
const onClick = jest.fn();
|
|
210
|
+
render(
|
|
211
|
+
<Checkbox
|
|
212
|
+
name="test"
|
|
213
|
+
renderInfoIcon={{ onClick, dataTestId: "info-btn" }}
|
|
214
|
+
/>
|
|
215
|
+
);
|
|
216
|
+
fireEvent.click(screen.getByTestId("info-btn"));
|
|
217
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("does not render info icon when renderInfoIcon is not provided", () => {
|
|
221
|
+
const { container } = render(<Checkbox name="test" />);
|
|
222
|
+
const buttons = container.querySelectorAll("button");
|
|
223
|
+
expect(buttons.length).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("containerClassName", () => {
|
|
228
|
+
it("applies containerClassName to the wrapper div", () => {
|
|
229
|
+
const { container } = render(
|
|
230
|
+
<Checkbox name="test" containerClassName="wrapper-class" />
|
|
231
|
+
);
|
|
232
|
+
const wrapper = container.firstChild as HTMLElement;
|
|
233
|
+
expect(wrapper.className).toContain("wrapper-class");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("checked icon rendering", () => {
|
|
238
|
+
it("renders checkmark SVG when checked", () => {
|
|
239
|
+
const { container } = render(<Checkbox name="test" checked={true} />);
|
|
240
|
+
const paths = container.querySelectorAll("path");
|
|
241
|
+
// Checked state has a checkmark path
|
|
242
|
+
expect(paths.length).toBeGreaterThan(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("renders empty box SVG when unchecked", () => {
|
|
246
|
+
const { container } = render(<Checkbox name="test" checked={false} />);
|
|
247
|
+
const rects = container.querySelectorAll("rect");
|
|
248
|
+
// Unchecked state has border rects
|
|
249
|
+
expect(rects.length).toBeGreaterThan(0);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/// <reference types="@testing-library/jest-dom" />
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Checklist } from "./index";
|
|
4
|
+
|
|
5
|
+
import { render, screen } from "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
jest.mock("@shared/components/list", () => ({
|
|
8
|
+
List: ({
|
|
9
|
+
children,
|
|
10
|
+
className,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}) => (
|
|
15
|
+
<ul data-testid="list" className={className}>
|
|
16
|
+
{children}
|
|
17
|
+
</ul>
|
|
18
|
+
),
|
|
19
|
+
ListItem: ({
|
|
20
|
+
children,
|
|
21
|
+
className,
|
|
22
|
+
}: {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
className?: string;
|
|
25
|
+
}) => (
|
|
26
|
+
<li data-testid="list-item" className={className}>
|
|
27
|
+
{children}
|
|
28
|
+
</li>
|
|
29
|
+
),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
jest.mock("@shared/components/material-icon", () => ({
|
|
33
|
+
MaterialIcon: ({
|
|
34
|
+
name,
|
|
35
|
+
size,
|
|
36
|
+
color,
|
|
37
|
+
className,
|
|
38
|
+
}: {
|
|
39
|
+
name: string;
|
|
40
|
+
size?: number;
|
|
41
|
+
color?: string;
|
|
42
|
+
className?: string;
|
|
43
|
+
}) => (
|
|
44
|
+
<span
|
|
45
|
+
data-testid={`icon-${name}`}
|
|
46
|
+
data-size={size}
|
|
47
|
+
data-color={color}
|
|
48
|
+
className={className}
|
|
49
|
+
>
|
|
50
|
+
{name}
|
|
51
|
+
</span>
|
|
52
|
+
),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
jest.mock("@shared/components/text", () => ({
|
|
56
|
+
Text: ({
|
|
57
|
+
children,
|
|
58
|
+
}: {
|
|
59
|
+
children: React.ReactNode;
|
|
60
|
+
as?: React.ElementType;
|
|
61
|
+
}) => <div data-testid="text-component">{children}</div>,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
jest.mock("@shared/utils/cx", () => ({
|
|
65
|
+
cx: (...args: unknown[]) => args.filter(Boolean).join(" "),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
describe("Checklist", () => {
|
|
69
|
+
describe("rendering", () => {
|
|
70
|
+
it("renders null when items is empty", () => {
|
|
71
|
+
const { container } = render(<Checklist items={[]} />);
|
|
72
|
+
expect(container.firstChild).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("renders null when items is undefined", () => {
|
|
76
|
+
const { container } = render(
|
|
77
|
+
<Checklist items={undefined as unknown as React.ReactNode[]} />
|
|
78
|
+
);
|
|
79
|
+
expect(container.firstChild).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("renders a list with items", () => {
|
|
83
|
+
render(<Checklist items={["Item 1", "Item 2", "Item 3"]} />);
|
|
84
|
+
const listItems = screen.getAllByTestId("list-item");
|
|
85
|
+
expect(listItems).toHaveLength(3);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("renders item text content", () => {
|
|
89
|
+
render(<Checklist items={["First item", "Second item"]} />);
|
|
90
|
+
expect(screen.getByText("First item")).toBeInTheDocument();
|
|
91
|
+
expect(screen.getByText("Second item")).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("renders ReactNode items", () => {
|
|
95
|
+
render(
|
|
96
|
+
<Checklist
|
|
97
|
+
items={[
|
|
98
|
+
<strong key="1">Bold item</strong>,
|
|
99
|
+
<em key="2">Italic item</em>,
|
|
100
|
+
]}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
expect(screen.getByText("Bold item")).toBeInTheDocument();
|
|
104
|
+
expect(screen.getByText("Italic item")).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("applies displayName correctly", () => {
|
|
108
|
+
expect(Checklist.displayName).toBe("Checklist");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("icons", () => {
|
|
113
|
+
it("renders check icons by default", () => {
|
|
114
|
+
render(<Checklist items={["Item 1"]} />);
|
|
115
|
+
expect(screen.getByTestId("icon-check")).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("renders custom icon name", () => {
|
|
119
|
+
render(<Checklist items={["Item 1"]} listIconName="check" />);
|
|
120
|
+
expect(screen.getByTestId("icon-check")).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not render icons when listIconName is disc", () => {
|
|
124
|
+
render(<Checklist items={["Item 1"]} listIconName="disc" />);
|
|
125
|
+
expect(screen.queryByTestId("icon-check")).not.toBeInTheDocument();
|
|
126
|
+
expect(screen.queryByTestId("icon-disc")).not.toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("passes correct iconSize to MaterialIcon", () => {
|
|
130
|
+
render(<Checklist items={["Item"]} iconSize={32} />);
|
|
131
|
+
const icon = screen.getByTestId("icon-check");
|
|
132
|
+
expect(icon).toHaveAttribute("data-size", "32");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("uses default iconSize of 20", () => {
|
|
136
|
+
render(<Checklist items={["Item"]} />);
|
|
137
|
+
const icon = screen.getByTestId("icon-check");
|
|
138
|
+
expect(icon).toHaveAttribute("data-size", "20");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("icon colors", () => {
|
|
143
|
+
it("applies green color by default", () => {
|
|
144
|
+
render(<Checklist items={["Item"]} />);
|
|
145
|
+
const icon = screen.getByTestId("icon-check");
|
|
146
|
+
expect(icon).toHaveAttribute("data-color", "#209A61");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("applies yellow color", () => {
|
|
150
|
+
render(<Checklist items={["Item"]} iconColor="yellow" />);
|
|
151
|
+
const icon = screen.getByTestId("icon-check");
|
|
152
|
+
expect(icon).toHaveAttribute("data-color", "#F5FF36");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("applies blue color", () => {
|
|
156
|
+
render(<Checklist items={["Item"]} iconColor="blue" />);
|
|
157
|
+
const icon = screen.getByTestId("icon-check");
|
|
158
|
+
expect(icon).toHaveAttribute("data-color", "#0393BA");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("icon position", () => {
|
|
163
|
+
it("applies items-center by default (center position)", () => {
|
|
164
|
+
render(<Checklist items={["Item"]} />);
|
|
165
|
+
const listItem = screen.getByTestId("list-item");
|
|
166
|
+
expect(listItem.className).toContain("items-center");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("applies items-start for top position", () => {
|
|
170
|
+
render(<Checklist items={["Item"]} iconPosition="top" />);
|
|
171
|
+
const listItem = screen.getByTestId("list-item");
|
|
172
|
+
expect(listItem.className).toContain("items-start");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("applies mt-1 to icon when position is top", () => {
|
|
176
|
+
render(<Checklist items={["Item"]} iconPosition="top" />);
|
|
177
|
+
const icon = screen.getByTestId("icon-check");
|
|
178
|
+
expect(icon.className).toContain("mt-1");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("does not apply mt-1 to icon when position is center", () => {
|
|
182
|
+
render(<Checklist items={["Item"]} iconPosition="center" />);
|
|
183
|
+
const icon = screen.getByTestId("icon-check");
|
|
184
|
+
expect(icon.className).not.toContain("mt-1");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("classNames", () => {
|
|
189
|
+
it("applies listContainerClassName to the List", () => {
|
|
190
|
+
render(
|
|
191
|
+
<Checklist items={["Item"]} listContainerClassName="custom-list" />
|
|
192
|
+
);
|
|
193
|
+
const list = screen.getByTestId("list");
|
|
194
|
+
expect(list.className).toContain("custom-list");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("applies listItemClassName to ListItems", () => {
|
|
198
|
+
render(
|
|
199
|
+
<Checklist
|
|
200
|
+
items={["Item 1", "Item 2"]}
|
|
201
|
+
listItemClassName="custom-item"
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
const items = screen.getAllByTestId("list-item");
|
|
205
|
+
items.forEach(item => {
|
|
206
|
+
expect(item.className).toContain("custom-item");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("applies iconClassName to the icon", () => {
|
|
211
|
+
render(<Checklist items={["Item"]} iconClassName="custom-icon" />);
|
|
212
|
+
const icon = screen.getByTestId("icon-check");
|
|
213
|
+
expect(icon.className).toContain("custom-icon");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("list variant", () => {
|
|
218
|
+
it("uses unstyled variant when icons are shown", () => {
|
|
219
|
+
render(<Checklist items={["Item"]} />);
|
|
220
|
+
// With icons (default check), the list items should have flex class
|
|
221
|
+
const listItem = screen.getByTestId("list-item");
|
|
222
|
+
expect(listItem.className).toContain("flex");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("does not apply flex class when using disc variant", () => {
|
|
226
|
+
render(<Checklist items={["Item"]} listIconName="disc" />);
|
|
227
|
+
const listItem = screen.getByTestId("list-item");
|
|
228
|
+
expect(listItem.className).not.toContain("flex");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ChecklistItem,
|
|
5
|
+
ChecklistProps,
|
|
6
|
+
iconColors,
|
|
7
|
+
} from "@shared/components/checklist/types";
|
|
4
8
|
import { List, ListItem } from "@shared/components/list";
|
|
5
9
|
import { MaterialIcon } from "@shared/components/material-icon";
|
|
6
10
|
import { Text } from "@shared/components/text";
|
|
7
11
|
import { cx } from "@shared/utils/cx";
|
|
8
12
|
|
|
13
|
+
function isChecklistItem(
|
|
14
|
+
item: React.ReactNode | ChecklistItem
|
|
15
|
+
): item is ChecklistItem {
|
|
16
|
+
return (
|
|
17
|
+
item !== null &&
|
|
18
|
+
typeof item === "object" &&
|
|
19
|
+
!React.isValidElement(item) &&
|
|
20
|
+
"title" in (item as Record<string, unknown>)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
export const Checklist: React.FC<ChecklistProps> = props => {
|
|
10
25
|
const {
|
|
11
26
|
items,
|
|
@@ -25,37 +40,57 @@ export const Checklist: React.FC<ChecklistProps> = props => {
|
|
|
25
40
|
className={cx("mt-2 space-y-1", listContainerClassName)}
|
|
26
41
|
variant={showIcons ? "unstyled" : "default"}
|
|
27
42
|
>
|
|
28
|
-
{items.map((
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
{items.map((item, idx) => {
|
|
44
|
+
const structured = isChecklistItem(item);
|
|
45
|
+
const hasCustomIcon = structured && !!item.iconUrl;
|
|
46
|
+
const content = structured ? item.title : item;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<ListItem
|
|
50
|
+
variant={showIcons ? "unstyled" : "default"}
|
|
51
|
+
key={idx}
|
|
52
|
+
className={cx(
|
|
53
|
+
`${showIcons ? "flex" : ""} ${iconPosition === "top" ? "items-start" : "items-center"} gap-2 text-text-secondary`,
|
|
54
|
+
listItemClassName
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{showIcons && hasCustomIcon ? (
|
|
58
|
+
<div className="flex shrink-0 leading-none">
|
|
59
|
+
<img
|
|
60
|
+
src={item.iconUrl}
|
|
61
|
+
alt={item.iconAlt || ""}
|
|
62
|
+
width={iconSize}
|
|
63
|
+
height={iconSize}
|
|
64
|
+
className={cx(
|
|
65
|
+
"block object-contain",
|
|
66
|
+
iconPosition === "top" ? "mt-1" : "",
|
|
67
|
+
iconClassName
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
) : showIcons ? (
|
|
72
|
+
<div className="flex leading-none">
|
|
73
|
+
<MaterialIcon
|
|
74
|
+
name={listIconName}
|
|
75
|
+
size={iconSize}
|
|
76
|
+
color={iconColors[iconColor]}
|
|
77
|
+
weight="600"
|
|
78
|
+
className={cx(
|
|
79
|
+
"block",
|
|
80
|
+
iconPosition === "top" ? "mt-1" : "",
|
|
81
|
+
iconClassName
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
) : null}
|
|
86
|
+
<Text as="div">{content}</Text>
|
|
87
|
+
</ListItem>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
55
90
|
</List>
|
|
56
91
|
);
|
|
57
92
|
};
|
|
58
93
|
|
|
59
94
|
Checklist.displayName = "Checklist";
|
|
60
95
|
|
|
61
|
-
export type { ChecklistProps };
|
|
96
|
+
export type { ChecklistItem, ChecklistProps };
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
export type ChecklistItem = {
|
|
2
|
+
title: React.ReactNode;
|
|
3
|
+
iconUrl?: string;
|
|
4
|
+
iconAlt?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
1
7
|
export type ChecklistProps = {
|
|
2
|
-
items: React.ReactNode[];
|
|
8
|
+
items: (React.ReactNode | ChecklistItem)[];
|
|
3
9
|
// TODO: add icon names as needed
|
|
4
10
|
listIconName?: "check" | "disc";
|
|
5
11
|
listItemClassName?: string;
|