@phsa.tec/design-system-react 0.1.2 → 0.1.3
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/.github/workflows/deploy-storybook.yml +54 -0
- package/README.md +13 -1
- package/package.json +5 -3
- package/src/components/dataInput/Input/components/Input/__tests__/input.test.tsx +38 -0
- package/src/components/dataInput/Input/components/Input/index.tsx +34 -248
- package/src/components/dataInput/Input/components/Input/types.ts +13 -0
- package/src/components/dataInput/Input/components/InputBase/index.tsx +14 -72
- package/src/components/dataInput/Input/components/MaskInput/__tests__/mask-input.test.tsx +54 -45
- package/src/components/dataInput/Input/components/MaskInput/mask-input.stories.tsx +15 -5
- package/src/components/dataInput/Input/components/MaskInput/mask-input.tsx +42 -31
- package/src/components/feedback/ErrorLabel/index.tsx +24 -0
- package/src/hooks/use-conditional-controller.tsx +35 -0
- package/src/components/dataInput/Input/components/Input/__tests__/Input.test.tsx +0 -100
- package/src/components/dataInput/Input/components/InputBase/__tests__/InputBase.test.tsx +0 -120
- package/src/components/dataInput/Input/components/NumberInput/__tests__/number-input.test.tsx +0 -95
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Deploy Storybook to GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pages: write
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: "pages"
|
|
16
|
+
cancel-in-progress: false
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Setup Node
|
|
26
|
+
uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: "20"
|
|
29
|
+
cache: "yarn"
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: yarn install --frozen-lockfile
|
|
33
|
+
|
|
34
|
+
- name: Build Storybook
|
|
35
|
+
run: yarn build-storybook
|
|
36
|
+
|
|
37
|
+
- name: Setup Pages
|
|
38
|
+
uses: actions/configure-pages@v4
|
|
39
|
+
|
|
40
|
+
- name: Upload artifact
|
|
41
|
+
uses: actions/upload-pages-artifact@v3
|
|
42
|
+
with:
|
|
43
|
+
path: "./storybook-static"
|
|
44
|
+
|
|
45
|
+
deploy:
|
|
46
|
+
environment:
|
|
47
|
+
name: github-pages
|
|
48
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
needs: build
|
|
51
|
+
steps:
|
|
52
|
+
- name: Deploy to GitHub Pages
|
|
53
|
+
id: deployment
|
|
54
|
+
uses: actions/deploy-pages@v4
|
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
A comprehensive React design system built with modern tools and best practices, featuring reusable components, consistent styling, and powerful layout solutions.
|
|
4
4
|
|
|
5
|
+
## 📚 Live Documentation
|
|
6
|
+
|
|
7
|
+
🌐 **[View Live Documentation](https://henriques4nti4go.github.io/phsa-design-system/)**
|
|
8
|
+
|
|
9
|
+
Explore all components interactively in our Storybook documentation, deployed automatically via GitHub Pages.
|
|
10
|
+
|
|
5
11
|
## ✨ Features
|
|
6
12
|
|
|
7
13
|
- 🧩 **Modular Components** - Reusable React components built with TypeScript
|
|
@@ -162,7 +168,13 @@ The README is written in English as requested and provides everything a develope
|
|
|
162
168
|
|
|
163
169
|
## 📚 Documentation
|
|
164
170
|
|
|
165
|
-
|
|
171
|
+
### Live Documentation
|
|
172
|
+
|
|
173
|
+
Visit our live Storybook documentation: **[https://henriques4nti4go.github.io/phsa-design-system/](https://henriques4nti4go.github.io/phsa-design-system/)**
|
|
174
|
+
|
|
175
|
+
### Local Development
|
|
176
|
+
|
|
177
|
+
Run Storybook locally to explore all available components:
|
|
166
178
|
|
|
167
179
|
```bash
|
|
168
180
|
npm run storybook
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phsa.tec/design-system-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"lint": "next lint",
|
|
11
11
|
"storybook": "storybook dev -p 6006",
|
|
12
12
|
"build-storybook": "storybook build",
|
|
13
|
+
"deploy-storybook": "storybook build && touch storybook-static/.nojekyll",
|
|
13
14
|
"test": "jest"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
@@ -62,7 +63,8 @@
|
|
|
62
63
|
"@storybook/addon-docs": "^9.0.15",
|
|
63
64
|
"@storybook/addon-onboarding": "^9.0.15",
|
|
64
65
|
"@storybook/nextjs": "^9.0.15",
|
|
65
|
-
"@testing-library/react": "^
|
|
66
|
+
"@testing-library/react": "^14.3.1",
|
|
67
|
+
"@testing-library/user-event": "^14.5.2",
|
|
66
68
|
"@types/jest": "^29.5.14",
|
|
67
69
|
"@types/lodash": "^4.17.15",
|
|
68
70
|
"@types/node": "^20",
|
|
@@ -73,7 +75,7 @@
|
|
|
73
75
|
"eslint": "^8",
|
|
74
76
|
"eslint-config-next": "15.0.3",
|
|
75
77
|
"eslint-plugin-storybook": "^9.0.15",
|
|
76
|
-
"jest": "^
|
|
78
|
+
"jest": "^30.0.4",
|
|
77
79
|
"jest-environment-jsdom": "^29.7.0",
|
|
78
80
|
"postcss": "^8",
|
|
79
81
|
"react-test-renderer": "^19.0.0",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import { Input } from "../index";
|
|
4
|
+
|
|
5
|
+
describe("Input", () => {
|
|
6
|
+
it("should render label", () => {
|
|
7
|
+
render(<Input label="test" data-testid="input" />);
|
|
8
|
+
const label = screen.getByTestId("input-label");
|
|
9
|
+
expect(label).toBeInTheDocument();
|
|
10
|
+
expect(label).toHaveTextContent("test");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should render error", () => {
|
|
14
|
+
render(<Input error="test" data-testid="input" />);
|
|
15
|
+
const error = screen.getByTestId("input-error-label");
|
|
16
|
+
expect(error).toBeInTheDocument();
|
|
17
|
+
expect(error).toHaveTextContent("test");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should is disabled", () => {
|
|
21
|
+
render(<Input disabled data-testid="input" />);
|
|
22
|
+
const input = screen.getByTestId("input");
|
|
23
|
+
expect(input).toBeDisabled();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should is required", () => {
|
|
27
|
+
render(<Input required data-testid="input" label="test" />);
|
|
28
|
+
const label = screen.getByTestId("input-label");
|
|
29
|
+
expect(label).toHaveTextContent("test *");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should change value", () => {
|
|
33
|
+
render(<Input data-testid="input" />);
|
|
34
|
+
const input = screen.getByTestId("input");
|
|
35
|
+
fireEvent.change(input, { target: { value: "test" } });
|
|
36
|
+
expect(input).toHaveValue("test");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -1,257 +1,43 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
FormField,
|
|
7
|
-
FormItem,
|
|
8
|
-
FormLabel,
|
|
9
|
-
FormMessage,
|
|
10
|
-
} from "../../../../../components/ui/form";
|
|
11
|
-
import { InputBase, InputBaseProps } from "./InputBase";
|
|
12
|
-
import { ErrorMessage } from "../../../../../components/dataDisplay/ErrorMessage";
|
|
1
|
+
import { Input as InputUI } from "@/components/ui/input";
|
|
2
|
+
import { InputProps } from "./types";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { InputBase } from "../InputBase";
|
|
5
|
+
import { useConditionalController } from "@/hooks/use-conditional-controller";
|
|
13
6
|
import { cn } from "@/lib/utils";
|
|
14
7
|
|
|
15
|
-
export type InputProps = Omit<InputBaseProps, "children"> & {
|
|
16
|
-
component?: React.ReactNode;
|
|
17
|
-
error?: string;
|
|
18
|
-
withoutForm?: boolean;
|
|
19
|
-
label?: string;
|
|
20
|
-
leftIcon?: React.ReactNode;
|
|
21
|
-
rightIcon?: React.ReactNode;
|
|
22
|
-
helperText?: string;
|
|
23
|
-
floatingLabel?: boolean;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
8
|
export const Input = ({
|
|
27
|
-
|
|
28
|
-
withoutForm,
|
|
29
|
-
component,
|
|
30
|
-
name,
|
|
31
|
-
error,
|
|
32
|
-
leftIcon,
|
|
33
|
-
rightIcon,
|
|
34
|
-
helperText,
|
|
35
|
-
floatingLabel = false,
|
|
36
|
-
"data-testid": testId,
|
|
37
|
-
className,
|
|
38
|
-
required,
|
|
39
|
-
placeholder,
|
|
9
|
+
"data-testid": dataTestId,
|
|
10
|
+
withoutForm = false,
|
|
40
11
|
...props
|
|
41
12
|
}: InputProps) => {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{/* Floating Label ou Label Normal */}
|
|
54
|
-
{label && !floatingLabel && (
|
|
55
|
-
<Label
|
|
56
|
-
htmlFor={name || "input"}
|
|
57
|
-
className={cn(
|
|
58
|
-
"block text-sm font-medium mb-1.5 transition-colors duration-200",
|
|
59
|
-
error ? "text-destructive" : "text-foreground"
|
|
60
|
-
)}
|
|
61
|
-
>
|
|
62
|
-
{label}
|
|
63
|
-
{required && <span className="text-destructive ml-1">*</span>}
|
|
64
|
-
</Label>
|
|
65
|
-
)}
|
|
66
|
-
|
|
67
|
-
{/* Input Container */}
|
|
68
|
-
<div className="relative">
|
|
69
|
-
{leftIcon && (
|
|
70
|
-
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
|
|
71
|
-
{leftIcon}
|
|
72
|
-
</div>
|
|
73
|
-
)}
|
|
74
|
-
|
|
75
|
-
<InputBase
|
|
76
|
-
{...props}
|
|
77
|
-
placeholder={floatingLabel ? " " : placeholder}
|
|
78
|
-
className={cn(
|
|
79
|
-
"w-full px-4 py-3 border border-border rounded-xl",
|
|
80
|
-
"bg-background transition-all duration-200",
|
|
81
|
-
"focus:border-primary focus:ring-4 focus:ring-primary/20",
|
|
82
|
-
"placeholder:text-muted-foreground",
|
|
83
|
-
"disabled:bg-muted disabled:text-muted-foreground",
|
|
84
|
-
leftIcon && "pl-10",
|
|
85
|
-
rightIcon && "pr-10",
|
|
86
|
-
error &&
|
|
87
|
-
"border-destructive focus:border-destructive focus:ring-destructive/20",
|
|
88
|
-
floatingLabel && "pt-6 pb-2"
|
|
89
|
-
)}
|
|
90
|
-
onFocus={(e) => {
|
|
91
|
-
setIsFocused(true);
|
|
92
|
-
props.onFocus?.(e);
|
|
93
|
-
}}
|
|
94
|
-
onBlur={(e) => {
|
|
95
|
-
setIsFocused(false);
|
|
96
|
-
setHasValue(!!e.target.value);
|
|
97
|
-
props.onBlur?.(e);
|
|
98
|
-
}}
|
|
99
|
-
/>
|
|
100
|
-
|
|
101
|
-
{/* Floating Label */}
|
|
102
|
-
{label && floatingLabel && (
|
|
103
|
-
<Label
|
|
104
|
-
htmlFor={name || "input"}
|
|
105
|
-
className={cn(
|
|
106
|
-
"absolute left-4 transition-all duration-200 pointer-events-none",
|
|
107
|
-
"text-muted-foreground",
|
|
108
|
-
isFocused || hasValue
|
|
109
|
-
? "top-2 text-xs font-medium"
|
|
110
|
-
: "top-1/2 transform -translate-y-1/2 text-base",
|
|
111
|
-
isFocused && !error && "text-primary",
|
|
112
|
-
error && "text-destructive"
|
|
113
|
-
)}
|
|
114
|
-
>
|
|
115
|
-
{label}
|
|
116
|
-
{required && <span className="text-destructive ml-1">*</span>}
|
|
117
|
-
</Label>
|
|
118
|
-
)}
|
|
119
|
-
|
|
120
|
-
{rightIcon && (
|
|
121
|
-
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
|
|
122
|
-
{rightIcon}
|
|
123
|
-
</div>
|
|
124
|
-
)}
|
|
125
|
-
|
|
126
|
-
{component && (
|
|
127
|
-
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 z-10">
|
|
128
|
-
{component}
|
|
129
|
-
</div>
|
|
130
|
-
)}
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
{/* Helper Text */}
|
|
134
|
-
{helperText && !error && (
|
|
135
|
-
<p className="mt-2 text-xs text-muted-foreground">{helperText}</p>
|
|
136
|
-
)}
|
|
137
|
-
|
|
138
|
-
{/* Error Message */}
|
|
139
|
-
{error && (
|
|
140
|
-
<div className="mt-2">
|
|
141
|
-
<ErrorMessage>{error}</ErrorMessage>
|
|
142
|
-
</div>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
);
|
|
13
|
+
const formData = useConditionalController({
|
|
14
|
+
name: props.name || "",
|
|
15
|
+
withoutForm,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const inputProps = useMemo(() => {
|
|
19
|
+
return {
|
|
20
|
+
...formData,
|
|
21
|
+
...props,
|
|
22
|
+
};
|
|
23
|
+
}, [formData, props]);
|
|
147
24
|
|
|
148
25
|
return (
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)}
|
|
166
|
-
data-testid={testId ? `form-label-${testId}` : undefined}
|
|
167
|
-
>
|
|
168
|
-
{label}
|
|
169
|
-
{required && <span className="text-destructive ml-1">*</span>}
|
|
170
|
-
</FormLabel>
|
|
171
|
-
)}
|
|
172
|
-
|
|
173
|
-
<FormControl>
|
|
174
|
-
<div className="relative">
|
|
175
|
-
{leftIcon && (
|
|
176
|
-
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
|
|
177
|
-
{leftIcon}
|
|
178
|
-
</div>
|
|
179
|
-
)}
|
|
180
|
-
|
|
181
|
-
<InputBase
|
|
182
|
-
{...props}
|
|
183
|
-
{...rest}
|
|
184
|
-
value={value}
|
|
185
|
-
onChangeText={onChange}
|
|
186
|
-
placeholder={floatingLabel ? " " : placeholder}
|
|
187
|
-
className={cn(
|
|
188
|
-
"w-full px-4 py-3 border border-border rounded-xl",
|
|
189
|
-
"bg-background transition-all duration-200",
|
|
190
|
-
"focus:border-primary focus:ring-4 focus:ring-primary/20",
|
|
191
|
-
"placeholder:text-muted-foreground",
|
|
192
|
-
"disabled:bg-muted disabled:text-muted-foreground",
|
|
193
|
-
leftIcon && "pl-10",
|
|
194
|
-
rightIcon && "pr-10",
|
|
195
|
-
floatingLabel && "pt-6 pb-2"
|
|
196
|
-
)}
|
|
197
|
-
onFocus={(e) => {
|
|
198
|
-
setIsFocused(true);
|
|
199
|
-
props.onFocus?.(e);
|
|
200
|
-
}}
|
|
201
|
-
onBlur={(e) => {
|
|
202
|
-
setIsFocused(false);
|
|
203
|
-
setHasValue(!!value);
|
|
204
|
-
props.onBlur?.(e);
|
|
205
|
-
}}
|
|
206
|
-
/>
|
|
207
|
-
|
|
208
|
-
{/* Floating Label */}
|
|
209
|
-
{label && floatingLabel && (
|
|
210
|
-
<FormLabel
|
|
211
|
-
htmlFor={name}
|
|
212
|
-
className={cn(
|
|
213
|
-
"absolute left-4 transition-all duration-200 pointer-events-none",
|
|
214
|
-
"text-muted-foreground",
|
|
215
|
-
isFocused || hasValue || value
|
|
216
|
-
? "top-2 text-xs font-medium"
|
|
217
|
-
: "top-1/2 transform -translate-y-1/2 text-base",
|
|
218
|
-
isFocused && "text-primary"
|
|
219
|
-
)}
|
|
220
|
-
>
|
|
221
|
-
{label}
|
|
222
|
-
{required && (
|
|
223
|
-
<span className="text-destructive ml-1">*</span>
|
|
224
|
-
)}
|
|
225
|
-
</FormLabel>
|
|
226
|
-
)}
|
|
227
|
-
|
|
228
|
-
{rightIcon && (
|
|
229
|
-
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
|
|
230
|
-
{rightIcon}
|
|
231
|
-
</div>
|
|
232
|
-
)}
|
|
233
|
-
|
|
234
|
-
{component && (
|
|
235
|
-
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 z-10">
|
|
236
|
-
{component}
|
|
237
|
-
</div>
|
|
238
|
-
)}
|
|
239
|
-
</div>
|
|
240
|
-
</FormControl>
|
|
241
|
-
|
|
242
|
-
{/* Helper Text */}
|
|
243
|
-
{helperText && (
|
|
244
|
-
<p className="mt-2 text-xs text-muted-foreground">{helperText}</p>
|
|
245
|
-
)}
|
|
246
|
-
|
|
247
|
-
<FormMessage
|
|
248
|
-
role="alert"
|
|
249
|
-
className="mt-2"
|
|
250
|
-
data-testid={testId ? `form-message-${testId}` : undefined}
|
|
251
|
-
/>
|
|
252
|
-
</div>
|
|
253
|
-
</FormItem>
|
|
254
|
-
)}
|
|
255
|
-
/>
|
|
26
|
+
<InputBase
|
|
27
|
+
label={props.label}
|
|
28
|
+
error={props.error}
|
|
29
|
+
required={props.required}
|
|
30
|
+
data-testid={dataTestId}
|
|
31
|
+
>
|
|
32
|
+
<InputUI
|
|
33
|
+
{...inputProps}
|
|
34
|
+
data-testid={dataTestId}
|
|
35
|
+
className={cn(
|
|
36
|
+
props.className,
|
|
37
|
+
props.error &&
|
|
38
|
+
"border-destructive focus:border-destructive focus-visible:ring-0"
|
|
39
|
+
)}
|
|
40
|
+
/>
|
|
41
|
+
</InputBase>
|
|
256
42
|
);
|
|
257
43
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { InputBaseProps } from "./InputBase";
|
|
2
|
+
|
|
3
|
+
export type InputProps = Omit<InputBaseProps, "children"> & {
|
|
4
|
+
component?: React.ReactNode;
|
|
5
|
+
error?: string;
|
|
6
|
+
withoutForm?: boolean;
|
|
7
|
+
label?: string;
|
|
8
|
+
leftIcon?: React.ReactNode;
|
|
9
|
+
rightIcon?: React.ReactNode;
|
|
10
|
+
helperText?: string;
|
|
11
|
+
floatingLabel?: boolean;
|
|
12
|
+
"data-testid"?: string;
|
|
13
|
+
};
|
|
@@ -1,89 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ErrorLabel } from "@/components/feedback/ErrorLabel";
|
|
2
2
|
import { InputProps as InputPropsUI } from "../../../../ui/input";
|
|
3
3
|
import { Label } from "../../../../ui/label";
|
|
4
|
-
import {
|
|
5
|
-
FormControl,
|
|
6
|
-
FormField,
|
|
7
|
-
FormItem,
|
|
8
|
-
FormLabel,
|
|
9
|
-
FormMessage,
|
|
10
|
-
} from "../../../../ui/form";
|
|
11
4
|
|
|
12
5
|
export type InputBaseProps = Omit<InputPropsUI, "children"> & {
|
|
13
6
|
label?: string;
|
|
14
|
-
withoutForm?: boolean;
|
|
15
|
-
"data-testid"?: string;
|
|
16
7
|
error?: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
HTMLInputElement
|
|
21
|
-
>
|
|
22
|
-
) => JSX.Element;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
"data-testid"?: string;
|
|
23
11
|
};
|
|
24
12
|
|
|
25
13
|
export const InputBase = ({
|
|
26
14
|
label,
|
|
27
|
-
withoutForm,
|
|
28
|
-
className,
|
|
29
|
-
name,
|
|
30
|
-
required,
|
|
31
|
-
"data-testid": testId,
|
|
32
15
|
error,
|
|
33
16
|
children,
|
|
34
|
-
|
|
17
|
+
required,
|
|
18
|
+
"data-testid": testId,
|
|
35
19
|
}: InputBaseProps) => {
|
|
36
|
-
const form = useFormContext();
|
|
37
|
-
const hasForm = !withoutForm && !!form && !!name;
|
|
38
|
-
|
|
39
|
-
if (!hasForm)
|
|
40
|
-
return (
|
|
41
|
-
<div className="grid w-full items-center gap-3">
|
|
42
|
-
<Label htmlFor="email">
|
|
43
|
-
{label}
|
|
44
|
-
{required && <span>*</span>}
|
|
45
|
-
</Label>
|
|
46
|
-
{children?.({
|
|
47
|
-
...props,
|
|
48
|
-
className,
|
|
49
|
-
name,
|
|
50
|
-
required,
|
|
51
|
-
})}
|
|
52
|
-
{error && <p className="text-red-500">{error}</p>}
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
|
|
56
20
|
return (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
className={className}
|
|
63
|
-
data-testid={testId ? `form-item-${testId}` : undefined}
|
|
64
|
-
>
|
|
65
|
-
{label && (
|
|
66
|
-
<FormLabel
|
|
67
|
-
htmlFor={name}
|
|
68
|
-
data-testid={testId ? `form-label-${testId}` : undefined}
|
|
69
|
-
>
|
|
70
|
-
{`${label}${required ? " *" : ""}`}
|
|
71
|
-
</FormLabel>
|
|
72
|
-
)}
|
|
73
|
-
<FormControl>
|
|
74
|
-
{children?.({
|
|
75
|
-
className,
|
|
76
|
-
required,
|
|
77
|
-
...props,
|
|
78
|
-
...field,
|
|
79
|
-
})}
|
|
80
|
-
</FormControl>
|
|
81
|
-
<FormMessage
|
|
82
|
-
role="alert"
|
|
83
|
-
data-testid={testId ? `form-message-${testId}` : undefined}
|
|
84
|
-
/>
|
|
85
|
-
</FormItem>
|
|
21
|
+
<div>
|
|
22
|
+
{label && (
|
|
23
|
+
<Label data-testid={`${testId}-label`}>
|
|
24
|
+
{`${label} ${required ? "*" : ""}`}{" "}
|
|
25
|
+
</Label>
|
|
86
26
|
)}
|
|
87
|
-
|
|
27
|
+
{children}
|
|
28
|
+
{error && <ErrorLabel data-testid={testId}>{error}</ErrorLabel>}
|
|
29
|
+
</div>
|
|
88
30
|
);
|
|
89
31
|
};
|
|
@@ -1,67 +1,76 @@
|
|
|
1
1
|
import "@testing-library/jest-dom";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import userEvent from "@testing-library/user-event";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
3
|
import { MaskInput } from "../mask-input";
|
|
5
4
|
|
|
6
5
|
describe("MaskInput", () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
mask
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
label
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
it("should render correctly", () => {
|
|
16
|
-
render(<MaskInput {...defaultProps} />);
|
|
17
|
-
|
|
18
|
-
expect(screen.getByTestId("input-base-cpf")).toBeInTheDocument();
|
|
19
|
-
expect(screen.getByTestId("input-wrapper-cpf")).toBeInTheDocument();
|
|
20
|
-
expect(screen.getByTestId("input-cpf")).toBeInTheDocument();
|
|
21
|
-
expect(screen.getByText("CPF")).toBeInTheDocument();
|
|
6
|
+
it("should render label", () => {
|
|
7
|
+
render(
|
|
8
|
+
<MaskInput label="test" data-testid="mask-input" mask="000.000.000-00" />
|
|
9
|
+
);
|
|
10
|
+
const label = screen.getByTestId("mask-input-label");
|
|
11
|
+
expect(label).toBeInTheDocument();
|
|
12
|
+
expect(label).toHaveTextContent("test");
|
|
22
13
|
});
|
|
23
14
|
|
|
24
|
-
it("should
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
expect(input).toHaveValue("123.456.789-00");
|
|
15
|
+
it("should render error", () => {
|
|
16
|
+
render(
|
|
17
|
+
<MaskInput error="test" data-testid="mask-input" mask="000.000.000-00" />
|
|
18
|
+
);
|
|
19
|
+
const error = screen.getByTestId("mask-input-error-label");
|
|
20
|
+
expect(error).toBeInTheDocument();
|
|
21
|
+
expect(error).toHaveTextContent("test");
|
|
32
22
|
});
|
|
33
23
|
|
|
34
|
-
it("should
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await user.type(input, "12345678900");
|
|
41
|
-
|
|
42
|
-
expect(onChange).toHaveBeenCalled();
|
|
24
|
+
it("should be disabled", () => {
|
|
25
|
+
render(
|
|
26
|
+
<MaskInput disabled data-testid="mask-input" mask="000.000.000-00" />
|
|
27
|
+
);
|
|
28
|
+
const input = screen.getByTestId("mask-input");
|
|
29
|
+
expect(input).toBeDisabled();
|
|
43
30
|
});
|
|
44
31
|
|
|
45
|
-
it("should
|
|
46
|
-
render(
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
it("should be required", () => {
|
|
33
|
+
render(
|
|
34
|
+
<MaskInput
|
|
35
|
+
required
|
|
36
|
+
data-testid="mask-input"
|
|
37
|
+
label="test"
|
|
38
|
+
mask="000.000.000-00"
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
const label = screen.getByTestId("mask-input-label");
|
|
42
|
+
expect(label).toHaveTextContent("test *");
|
|
49
43
|
});
|
|
50
44
|
|
|
51
|
-
it("should
|
|
52
|
-
render(<MaskInput
|
|
45
|
+
it("should apply mask to input value", async () => {
|
|
46
|
+
render(<MaskInput data-testid="mask-input" mask="000.000.000-00" />);
|
|
47
|
+
const input = screen.getByTestId("mask-input");
|
|
48
|
+
|
|
49
|
+
fireEvent.focus(input);
|
|
50
|
+
fireEvent.input(input, { target: { value: "12345678901" } });
|
|
53
51
|
|
|
54
|
-
expect(
|
|
52
|
+
expect(input).toHaveValue("123.456.789-01");
|
|
55
53
|
});
|
|
56
54
|
|
|
57
|
-
it("should
|
|
55
|
+
it("should handle placeholder", () => {
|
|
58
56
|
render(
|
|
59
57
|
<MaskInput
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
data-testid="mask-input"
|
|
59
|
+
mask="000.000.000-00"
|
|
60
|
+
placeholder="Digite seu CPF"
|
|
62
61
|
/>
|
|
63
62
|
);
|
|
63
|
+
const input = screen.getByTestId("mask-input");
|
|
64
|
+
|
|
65
|
+
expect(input).toHaveAttribute("placeholder", "Digite seu CPF");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should handle name attribute", () => {
|
|
69
|
+
render(
|
|
70
|
+
<MaskInput data-testid="mask-input" mask="000.000.000-00" name="cpf" />
|
|
71
|
+
);
|
|
72
|
+
const input = screen.getByTestId("mask-input");
|
|
64
73
|
|
|
65
|
-
expect(
|
|
74
|
+
expect(input).toHaveAttribute("name", "cpf");
|
|
66
75
|
});
|
|
67
76
|
});
|
|
@@ -16,31 +16,41 @@ export default meta;
|
|
|
16
16
|
type Story = StoryObj<typeof meta>;
|
|
17
17
|
|
|
18
18
|
export const Default: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
placeholder: "99999-999",
|
|
21
|
+
label: "CEP",
|
|
22
|
+
mask: "00000-000",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const CPF: Story = {
|
|
19
27
|
args: {
|
|
20
28
|
placeholder: "000.000.000-00",
|
|
21
29
|
label: "CPF",
|
|
22
|
-
mask: "
|
|
30
|
+
mask: "000.000.000-00",
|
|
23
31
|
},
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
export const Phone: Story = {
|
|
27
35
|
args: {
|
|
28
|
-
placeholder: "(
|
|
36
|
+
placeholder: "(00) 00000-0000",
|
|
29
37
|
label: "Telefone",
|
|
38
|
+
mask: "(00) 00000-0000",
|
|
30
39
|
},
|
|
31
40
|
};
|
|
32
41
|
|
|
33
42
|
export const Date: Story = {
|
|
34
43
|
args: {
|
|
35
|
-
placeholder: "
|
|
44
|
+
placeholder: "00/00/0000",
|
|
36
45
|
label: "Data",
|
|
46
|
+
mask: "00/00/0000",
|
|
37
47
|
},
|
|
38
48
|
};
|
|
39
49
|
|
|
40
50
|
export const WithForm = () => {
|
|
41
51
|
const form = useForm({
|
|
42
52
|
defaultValues: {
|
|
43
|
-
cpf: "
|
|
53
|
+
cpf: "",
|
|
44
54
|
},
|
|
45
55
|
});
|
|
46
56
|
|
|
@@ -51,7 +61,7 @@ export const WithForm = () => {
|
|
|
51
61
|
name="cpf"
|
|
52
62
|
label="CPF"
|
|
53
63
|
placeholder="000.000.000-00"
|
|
54
|
-
mask="
|
|
64
|
+
mask="000.000.000-00"
|
|
55
65
|
/>
|
|
56
66
|
</form>
|
|
57
67
|
</Form>
|
|
@@ -1,43 +1,54 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useIMask, ReactMaskOpts } from "react-imask";
|
|
4
|
+
import { Input } from "../Input";
|
|
5
|
+
import { useConditionalController } from "@/hooks/use-conditional-controller";
|
|
6
|
+
import { InputProps } from "../Input/types";
|
|
5
7
|
|
|
6
|
-
export type MaskInputProps =
|
|
7
|
-
placeholder?: string;
|
|
8
|
-
className?: string;
|
|
9
|
-
withoutForm?: boolean;
|
|
10
|
-
component?: React.ReactNode;
|
|
11
|
-
"data-testid"?: string;
|
|
12
|
-
onChange?: (value: string) => void;
|
|
13
|
-
};
|
|
8
|
+
export type MaskInputProps = ReactMaskOpts & InputProps;
|
|
14
9
|
|
|
15
10
|
export const MaskInput = ({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
label,
|
|
19
|
-
name,
|
|
20
|
-
withoutForm,
|
|
11
|
+
"data-testid": dataTestId,
|
|
12
|
+
withoutForm = false,
|
|
21
13
|
...props
|
|
22
14
|
}: MaskInputProps) => {
|
|
15
|
+
const formData = useConditionalController({
|
|
16
|
+
name: props.name || "",
|
|
17
|
+
withoutForm,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
label,
|
|
22
|
+
error,
|
|
23
|
+
required,
|
|
24
|
+
name,
|
|
25
|
+
className,
|
|
26
|
+
placeholder,
|
|
27
|
+
...imaskProps
|
|
28
|
+
} = props;
|
|
29
|
+
|
|
30
|
+
const { ref, value } = useIMask({
|
|
31
|
+
...imaskProps,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (formData.value !== undefined && formData.value !== value) {
|
|
36
|
+
formData.onChange(value);
|
|
37
|
+
}
|
|
38
|
+
}, [formData, value]);
|
|
39
|
+
|
|
23
40
|
return (
|
|
24
|
-
<
|
|
41
|
+
<Input
|
|
42
|
+
{...props}
|
|
43
|
+
ref={ref as React.RefObject<HTMLInputElement>}
|
|
44
|
+
data-testid={dataTestId}
|
|
45
|
+
withoutForm={true}
|
|
25
46
|
label={label}
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
error={error}
|
|
48
|
+
required={required}
|
|
49
|
+
className={className}
|
|
50
|
+
placeholder={placeholder}
|
|
28
51
|
name={name}
|
|
29
|
-
|
|
30
|
-
{({ ...rest }) => {
|
|
31
|
-
return (
|
|
32
|
-
<div
|
|
33
|
-
className="flex w-full gap-3"
|
|
34
|
-
data-testid={`input-wrapper-${testId}`}
|
|
35
|
-
>
|
|
36
|
-
<Input {...props} {...rest} />
|
|
37
|
-
{component}
|
|
38
|
-
</div>
|
|
39
|
-
);
|
|
40
|
-
}}
|
|
41
|
-
</InputBase>
|
|
52
|
+
/>
|
|
42
53
|
);
|
|
43
54
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "@/components/dataDisplay";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export type ErrorLabelProps = React.PropsWithChildren<{
|
|
6
|
+
className?: string;
|
|
7
|
+
"data-testid"?: string;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export const ErrorLabel = ({
|
|
11
|
+
children,
|
|
12
|
+
className,
|
|
13
|
+
"data-testid": dataTestId,
|
|
14
|
+
}: ErrorLabelProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className={cn("flex items-center gap-2 my-2", className)}
|
|
18
|
+
data-testid={`${dataTestId}-error-label`}
|
|
19
|
+
>
|
|
20
|
+
<Icon name="MdErrorOutline" size={18} className="text-destructive" />
|
|
21
|
+
<span className="text-destructive text-sm">{children}</span>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useController,
|
|
4
|
+
useForm,
|
|
5
|
+
useFormContext,
|
|
6
|
+
ControllerRenderProps,
|
|
7
|
+
FieldValues,
|
|
8
|
+
} from "react-hook-form";
|
|
9
|
+
|
|
10
|
+
export const useConditionalController = ({
|
|
11
|
+
name,
|
|
12
|
+
withoutForm,
|
|
13
|
+
}: {
|
|
14
|
+
name: string;
|
|
15
|
+
withoutForm?: boolean;
|
|
16
|
+
}): ControllerRenderProps<FieldValues, string> | Record<string, never> => {
|
|
17
|
+
const form = useFormContext();
|
|
18
|
+
|
|
19
|
+
const hasForm = useMemo(() => {
|
|
20
|
+
return !withoutForm && !!form?.control;
|
|
21
|
+
}, [withoutForm, form]);
|
|
22
|
+
|
|
23
|
+
const tempForm = useForm();
|
|
24
|
+
|
|
25
|
+
const controlToUse = useMemo(() => {
|
|
26
|
+
return hasForm ? form.control : tempForm.control;
|
|
27
|
+
}, [tempForm, form, hasForm]);
|
|
28
|
+
|
|
29
|
+
const controller = useController({
|
|
30
|
+
control: controlToUse,
|
|
31
|
+
name: name || "temp",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return hasForm ? controller.field : {};
|
|
35
|
+
};
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import userEvent from "@testing-library/user-event";
|
|
4
|
-
import { Input } from "../index";
|
|
5
|
-
import { FormProvider, useForm } from "react-hook-form";
|
|
6
|
-
|
|
7
|
-
// Componente wrapper para testar o Input dentro de um formulário
|
|
8
|
-
const FormWrapper = ({ children }: { children: React.ReactNode }) => {
|
|
9
|
-
const methods = useForm();
|
|
10
|
-
return <FormProvider {...methods}>{children}</FormProvider>;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
describe("Componente Input", () => {
|
|
14
|
-
it("deve renderizar com label", () => {
|
|
15
|
-
render(
|
|
16
|
-
<FormWrapper>
|
|
17
|
-
<Input label="Nome" name="name" />
|
|
18
|
-
</FormWrapper>
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
expect(screen.getByLabelText("Nome")).toBeInTheDocument();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("deve renderizar sem formulário quando withoutForm é true", () => {
|
|
25
|
-
render(<Input label="Email" name="email" withoutForm={true} />);
|
|
26
|
-
|
|
27
|
-
expect(screen.getByLabelText("Email")).toBeInTheDocument();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("deve exibir mensagem de erro quando fornecida", () => {
|
|
31
|
-
render(
|
|
32
|
-
<FormWrapper>
|
|
33
|
-
<Input label="Senha" name="password" error="Senha é obrigatória" />
|
|
34
|
-
</FormWrapper>
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
expect(screen.getByText("Senha é obrigatória")).toBeInTheDocument();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("deve aplicar data-testid quando fornecido", () => {
|
|
41
|
-
render(
|
|
42
|
-
<FormWrapper>
|
|
43
|
-
<Input label="Telefone" name="phone" data-testid="phone-input" />
|
|
44
|
-
</FormWrapper>
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
expect(screen.getByTestId("phone-input")).toBeInTheDocument();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("deve lidar corretamente com a entrada do usuário", async () => {
|
|
51
|
-
const user = userEvent.setup();
|
|
52
|
-
|
|
53
|
-
render(
|
|
54
|
-
<FormWrapper>
|
|
55
|
-
<Input label="Usuário" name="username" />
|
|
56
|
-
</FormWrapper>
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const input = screen.getByLabelText("Usuário");
|
|
60
|
-
await user.type(input, "testuser");
|
|
61
|
-
|
|
62
|
-
expect(input).toHaveValue("testuser");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("deve passar props adicionais para o elemento input", () => {
|
|
66
|
-
render(
|
|
67
|
-
<FormWrapper>
|
|
68
|
-
<Input
|
|
69
|
-
label="Código"
|
|
70
|
-
name="code"
|
|
71
|
-
placeholder="Digite o código"
|
|
72
|
-
maxLength={6}
|
|
73
|
-
/>
|
|
74
|
-
</FormWrapper>
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const input = screen.getByLabelText("Código");
|
|
78
|
-
expect(input).toHaveAttribute("placeholder", "Digite o código");
|
|
79
|
-
expect(input).toHaveAttribute("maxLength", "6");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("deve chamar o onChange quando usado sem formulário", async () => {
|
|
83
|
-
const handleChange = jest.fn();
|
|
84
|
-
const user = userEvent.setup();
|
|
85
|
-
|
|
86
|
-
render(
|
|
87
|
-
<Input
|
|
88
|
-
label="Comentário"
|
|
89
|
-
name="comment"
|
|
90
|
-
withoutForm={true}
|
|
91
|
-
onChange={handleChange}
|
|
92
|
-
/>
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const input = screen.getByLabelText("Comentário");
|
|
96
|
-
await user.type(input, "a");
|
|
97
|
-
|
|
98
|
-
expect(handleChange).toHaveBeenCalled();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import "@testing-library/jest-dom";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { render, screen } from "@testing-library/react";
|
|
4
|
-
import { InputBase, CustomInputProps } from "..";
|
|
5
|
-
import { Form } from "../../../../../../components/ui/form";
|
|
6
|
-
import { useForm } from "react-hook-form";
|
|
7
|
-
|
|
8
|
-
describe("InputBase", () => {
|
|
9
|
-
const MockInput = (props: CustomInputProps) => (
|
|
10
|
-
<input
|
|
11
|
-
data-testid="mock-input"
|
|
12
|
-
{...props}
|
|
13
|
-
onChange={(e) => props.onChange?.(e.target.value)}
|
|
14
|
-
/>
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
it("deve renderizar sem contexto de formulário", () => {
|
|
18
|
-
render(<InputBase data-testid="test">{() => <MockInput />}</InputBase>);
|
|
19
|
-
expect(screen.getByTestId("mock-input")).toBeInTheDocument();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("deve renderizar label e descrição", () => {
|
|
23
|
-
render(
|
|
24
|
-
<InputBase
|
|
25
|
-
label="Test Label"
|
|
26
|
-
description="Test Description"
|
|
27
|
-
data-testid="test"
|
|
28
|
-
>
|
|
29
|
-
{() => <MockInput />}
|
|
30
|
-
</InputBase>
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
expect(screen.getByText("Test Label")).toBeInTheDocument();
|
|
34
|
-
expect(screen.getByText("Test Description")).toBeInTheDocument();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("deve mostrar indicador de obrigatório quando required é true", () => {
|
|
38
|
-
render(
|
|
39
|
-
<InputBase label="Test Label" required data-testid="test">
|
|
40
|
-
{() => <MockInput />}
|
|
41
|
-
</InputBase>
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
expect(screen.getByText("Test Label *")).toBeInTheDocument();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("deve renderizar mensagem de erro", () => {
|
|
48
|
-
render(
|
|
49
|
-
<InputBase error="Test Error" data-testid="test">
|
|
50
|
-
{() => <MockInput />}
|
|
51
|
-
</InputBase>
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
expect(screen.getByText("Test Error")).toBeInTheDocument();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("deve funcionar com contexto de formulário", () => {
|
|
58
|
-
const TestForm = () => {
|
|
59
|
-
const form = useForm();
|
|
60
|
-
return (
|
|
61
|
-
<Form {...form}>
|
|
62
|
-
<InputBase name="test" data-testid="test">
|
|
63
|
-
{() => <MockInput />}
|
|
64
|
-
</InputBase>
|
|
65
|
-
</Form>
|
|
66
|
-
);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
render(<TestForm />);
|
|
70
|
-
expect(screen.getByTestId("mock-input")).toBeInTheDocument();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("deve aplicar className customizada", () => {
|
|
74
|
-
render(
|
|
75
|
-
<InputBase className="custom-class" data-testid="test">
|
|
76
|
-
{() => <MockInput />}
|
|
77
|
-
</InputBase>
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const container = screen.getByTestId("input-base-test");
|
|
81
|
-
expect(container).toHaveClass("custom-class");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("deve lidar com estado disabled", () => {
|
|
85
|
-
render(
|
|
86
|
-
<InputBase disabled data-testid="mock-input">
|
|
87
|
-
{(props) => <MockInput {...props} />}
|
|
88
|
-
</InputBase>
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
expect(screen.getByTestId("mock-input")).toBeDisabled();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("deve validar erros do React Hook Form", async () => {
|
|
95
|
-
const TestForm = () => {
|
|
96
|
-
const form = useForm({
|
|
97
|
-
defaultValues: {
|
|
98
|
-
test: "",
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
React.useEffect(() => {
|
|
103
|
-
form.setError("test", { message: "Test Error" });
|
|
104
|
-
}, [form]);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<Form {...form}>
|
|
108
|
-
<InputBase name="test" data-testid="mock-input" required>
|
|
109
|
-
{(props) => <MockInput {...props} />}
|
|
110
|
-
</InputBase>
|
|
111
|
-
</Form>
|
|
112
|
-
);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
render(<TestForm />);
|
|
116
|
-
|
|
117
|
-
const errorMessage = await screen.findByText("Test Error");
|
|
118
|
-
expect(errorMessage).toBeInTheDocument();
|
|
119
|
-
});
|
|
120
|
-
});
|
package/src/components/dataInput/Input/components/NumberInput/__tests__/number-input.test.tsx
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import "@testing-library/jest-dom";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { NumberInput } from "../number-input";
|
|
4
|
-
import userEvent from "@testing-library/user-event";
|
|
5
|
-
|
|
6
|
-
describe("NumberInput", () => {
|
|
7
|
-
it("deve renderizar o input com label", () => {
|
|
8
|
-
render(<NumberInput label="Valor" data-testid="test" />);
|
|
9
|
-
|
|
10
|
-
expect(screen.getByTestId("test-number-input")).toBeInTheDocument();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("deve renderizar a descrição quando fornecida", () => {
|
|
14
|
-
render(<NumberInput description="Digite um número" />);
|
|
15
|
-
|
|
16
|
-
expect(screen.getByText("Digite um número")).toBeInTheDocument();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("deve exibir mensagem de erro quando fornecida", () => {
|
|
20
|
-
render(<NumberInput error="Valor inválido" />);
|
|
21
|
-
|
|
22
|
-
expect(screen.getByText("Valor inválido")).toBeInTheDocument();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("deve chamar onValueChange com o valor numérico correto", async () => {
|
|
26
|
-
const onValueChange = jest.fn();
|
|
27
|
-
render(<NumberInput onChange={onValueChange} />);
|
|
28
|
-
|
|
29
|
-
const input = screen.getByRole("textbox");
|
|
30
|
-
await userEvent.type(input, "123.45");
|
|
31
|
-
|
|
32
|
-
expect(onValueChange).toHaveBeenCalledWith(123.45);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("deve aceitar apenas números e separador decimal", async () => {
|
|
36
|
-
const onValueChange = jest.fn();
|
|
37
|
-
render(<NumberInput onChange={onValueChange} />);
|
|
38
|
-
|
|
39
|
-
const input = screen.getByRole("textbox");
|
|
40
|
-
await userEvent.type(input, "abc123.45def");
|
|
41
|
-
|
|
42
|
-
expect(input).toHaveValue("123.45");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("deve renderizar componente adicional quando fornecido", () => {
|
|
46
|
-
render(
|
|
47
|
-
<NumberInput
|
|
48
|
-
component={<button data-testid="extra-component">Extra</button>}
|
|
49
|
-
/>
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
expect(screen.getByTestId("extra-component")).toBeInTheDocument();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("deve aplicar className customizada", () => {
|
|
56
|
-
render(<NumberInput className="custom-class" />);
|
|
57
|
-
|
|
58
|
-
expect(
|
|
59
|
-
screen.getByRole("textbox").parentElement?.parentElement
|
|
60
|
-
).toHaveClass("custom-class");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("deve formatar o valor inicial corretamente", () => {
|
|
64
|
-
render(
|
|
65
|
-
<NumberInput
|
|
66
|
-
value={1234.56}
|
|
67
|
-
data-testid="test"
|
|
68
|
-
thousandSeparator="."
|
|
69
|
-
decimalSeparator=","
|
|
70
|
-
fixedDecimalScale
|
|
71
|
-
decimalScale={2}
|
|
72
|
-
/>
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
expect(screen.getByTestId("test-number-input")).toHaveValue("1.234,56");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("deve formatar o valor conforme digitação", async () => {
|
|
79
|
-
const user = userEvent.setup();
|
|
80
|
-
render(
|
|
81
|
-
<NumberInput
|
|
82
|
-
data-testid="test"
|
|
83
|
-
thousandSeparator="."
|
|
84
|
-
decimalSeparator=","
|
|
85
|
-
fixedDecimalScale
|
|
86
|
-
decimalScale={2}
|
|
87
|
-
/>
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const input = screen.getByTestId("test-number-input");
|
|
91
|
-
await user.type(input, "123456");
|
|
92
|
-
|
|
93
|
-
expect(input).toHaveValue("123.456,00");
|
|
94
|
-
});
|
|
95
|
-
});
|