@hero-design/rn-work-uikit 1.2.0-alpha.1 → 1.2.0
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/CHANGELOG.md +8 -8
- package/lib/index.js +2102 -186
- package/package.json +2 -2
- package/rollup.config.mjs +0 -1
- package/src/__tests__/__snapshots__/index.spec.tsx.snap +10 -29
- package/src/components/TextInput/FloatingLabel.tsx +8 -2
- package/src/components/TextInput/Group/__tests__/__snapshots__/index.spec.tsx.snap +883 -0
- package/src/components/TextInput/Group/__tests__/index.spec.tsx +179 -0
- package/src/components/TextInput/Group/__tests__/utils.spec.ts +73 -0
- package/src/components/TextInput/Group/index.tsx +102 -0
- package/src/components/TextInput/Group/utils.ts +67 -0
- package/src/components/TextInput/InputRow.tsx +47 -29
- package/src/components/TextInput/MIGRATION.md +133 -0
- package/src/components/TextInput/StyledTextInput.tsx +36 -8
- package/src/components/TextInput/__tests__/FloatingLabel.spec.tsx +36 -46
- package/src/components/TextInput/__tests__/InputRow.spec.tsx +50 -95
- package/src/components/TextInput/__tests__/__snapshots__/index.spec.tsx.snap +559 -544
- package/src/components/TextInput/__tests__/index.spec.tsx +241 -262
- package/src/components/TextInput/index.tsx +39 -17
- package/src/components/TextInput/types.ts +7 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { act, fireEvent, within } from '@testing-library/react-native';
|
|
2
|
+
import { act, fireEvent, waitFor, within } from '@testing-library/react-native';
|
|
3
3
|
import {
|
|
4
4
|
TextInput as RNTextInput,
|
|
5
5
|
ViewStyle,
|
|
@@ -14,57 +14,40 @@ import TextInput, { TextInputHandles } from '../index';
|
|
|
14
14
|
describe('TextInput', () => {
|
|
15
15
|
describe('when user sees an empty input field', () => {
|
|
16
16
|
it('should display label and icons but hide input content until user interacts', () => {
|
|
17
|
-
const { toJSON, getByTestId } = renderWithTheme(
|
|
17
|
+
const { toJSON, getByTestId, queryAllByTestId } = renderWithTheme(
|
|
18
18
|
<TextInput
|
|
19
19
|
label="Amount (AUD)"
|
|
20
20
|
prefix="dollar-sign"
|
|
21
21
|
suffix="arrow-down"
|
|
22
|
-
testID="idle-text-input"
|
|
23
22
|
style={{ borderColor: theme.colors.error }}
|
|
24
23
|
/>
|
|
25
24
|
);
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
const inputContainer = getByTestId('idle-text-input');
|
|
29
|
-
expect(inputContainer).toBeTruthy();
|
|
30
|
-
|
|
31
|
-
// User should see all visual elements
|
|
32
|
-
const textInput = within(inputContainer).getByTestId('text-input');
|
|
33
|
-
const inputPrefix = within(inputContainer).getByTestId('input-prefix');
|
|
34
|
-
const inputLabel = within(inputContainer).getByTestId('input-label');
|
|
35
|
-
const inputSuffix = within(inputContainer).getByTestId('input-suffix');
|
|
36
|
-
const inputRowWrapper = within(inputContainer).getByTestId(
|
|
37
|
-
'input-row-input-wrapper'
|
|
38
|
-
);
|
|
39
|
-
|
|
26
|
+
const textInput = getByTestId('text-input');
|
|
40
27
|
expect(textInput).toBeTruthy();
|
|
41
|
-
expect(inputPrefix).toBeTruthy();
|
|
42
|
-
expect(inputLabel).toBeTruthy();
|
|
43
|
-
expect(inputSuffix).toBeTruthy();
|
|
44
28
|
|
|
45
|
-
//
|
|
46
|
-
expect(
|
|
47
|
-
|
|
48
|
-
).
|
|
29
|
+
// Ensure all visual elements are present
|
|
30
|
+
expect(queryAllByTestId('input-prefix')).toHaveLength(1);
|
|
31
|
+
expect(queryAllByTestId('input-label')).toHaveLength(1);
|
|
32
|
+
expect(queryAllByTestId('input-suffix')).toHaveLength(1);
|
|
49
33
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
expect(
|
|
34
|
+
// User should see the label text
|
|
35
|
+
const inputLabel = getByTestId('input-label');
|
|
36
|
+
expect(inputLabel).toHaveTextContent('Amount (AUD)');
|
|
53
37
|
|
|
54
38
|
// User should be able to identify the prefix icon through accessibility
|
|
55
|
-
const prefixIcon =
|
|
39
|
+
const prefixIcon = getByTestId('input-prefix-icon');
|
|
40
|
+
expect(prefixIcon).toBeTruthy();
|
|
41
|
+
expect(prefixIcon).toHaveProp(
|
|
42
|
+
'accessibilityLabel',
|
|
56
43
|
'Prefix icon: dollar-sign'
|
|
57
44
|
);
|
|
58
|
-
expect(prefixIcon).toBeTruthy();
|
|
59
|
-
expect(
|
|
60
|
-
within(inputContainer).getByTestId('input-prefix-icon')
|
|
61
|
-
).toBeTruthy();
|
|
62
45
|
|
|
63
46
|
expect(toJSON()).toMatchSnapshot();
|
|
64
47
|
});
|
|
65
48
|
|
|
66
49
|
it('should not show label when no label text is provided', () => {
|
|
67
|
-
const {
|
|
50
|
+
const { queryByTestId, queryAllByTestId } = renderWithTheme(
|
|
68
51
|
<TextInput
|
|
69
52
|
prefix="dollar-sign"
|
|
70
53
|
suffix="arrow-down"
|
|
@@ -72,21 +55,12 @@ describe('TextInput', () => {
|
|
|
72
55
|
/>
|
|
73
56
|
);
|
|
74
57
|
|
|
75
|
-
const inputContainer = getByTestId('idle-text-input');
|
|
76
|
-
expect(inputContainer).toBeTruthy();
|
|
77
|
-
|
|
78
58
|
// User should not see any label element when none is provided
|
|
79
|
-
expect(
|
|
80
|
-
within(inputContainer).queryAllByTestId('input-label')
|
|
81
|
-
).toHaveLength(0);
|
|
59
|
+
expect(queryAllByTestId('input-label')).toHaveLength(0);
|
|
82
60
|
|
|
83
61
|
// Input elements should still be present but hidden until interaction
|
|
84
|
-
expect(
|
|
85
|
-
|
|
86
|
-
).toHaveLength(1);
|
|
87
|
-
expect(
|
|
88
|
-
within(inputContainer).queryAllByTestId('input-prefix')
|
|
89
|
-
).toHaveLength(1);
|
|
62
|
+
expect(queryByTestId('text-input')).toBeTruthy();
|
|
63
|
+
expect(queryByTestId('input-prefix')).toBeTruthy();
|
|
90
64
|
});
|
|
91
65
|
|
|
92
66
|
it('should respond to user interactions with proper callbacks', () => {
|
|
@@ -99,19 +73,17 @@ describe('TextInput', () => {
|
|
|
99
73
|
label="Amount (AUD)"
|
|
100
74
|
prefix="dollar-sign"
|
|
101
75
|
suffix="arrow-down"
|
|
102
|
-
testID="idle-text-input"
|
|
103
76
|
onChangeText={onChangeText}
|
|
104
77
|
onBlur={onBlur}
|
|
105
78
|
onFocus={onFocus}
|
|
106
79
|
/>
|
|
107
80
|
);
|
|
108
81
|
|
|
109
|
-
const
|
|
110
|
-
const labelElement = within(inputContainer).getByTestId('input-label');
|
|
111
|
-
const textInput = within(inputContainer).getByTestId('text-input');
|
|
82
|
+
const textInput = getByTestId('text-input');
|
|
112
83
|
|
|
113
|
-
// User
|
|
114
|
-
fireEvent
|
|
84
|
+
// User focuses the input
|
|
85
|
+
fireEvent(textInput, 'focus');
|
|
86
|
+
expect(onFocus).toHaveBeenCalledTimes(1);
|
|
115
87
|
|
|
116
88
|
// User types text into the input
|
|
117
89
|
fireEvent.changeText(textInput, 'Thong Quach');
|
|
@@ -120,16 +92,12 @@ describe('TextInput', () => {
|
|
|
120
92
|
// User moves focus away from the input
|
|
121
93
|
fireEvent(textInput, 'blur');
|
|
122
94
|
expect(onBlur).toHaveBeenCalledTimes(1);
|
|
123
|
-
|
|
124
|
-
// User focuses the input again
|
|
125
|
-
fireEvent(textInput, 'focus');
|
|
126
|
-
expect(onFocus).toHaveBeenCalledTimes(1);
|
|
127
95
|
});
|
|
128
96
|
});
|
|
129
97
|
|
|
130
98
|
describe('when user sees input with custom prefix and suffix elements', () => {
|
|
131
99
|
it('should display custom React components instead of icon names', () => {
|
|
132
|
-
const { toJSON,
|
|
100
|
+
const { toJSON, getByTestId } = renderWithTheme(
|
|
133
101
|
<TextInput
|
|
134
102
|
label="Amount (AUD)"
|
|
135
103
|
prefix={<Icon icon="eye-circle" testID="prefix-element" />}
|
|
@@ -138,15 +106,13 @@ describe('TextInput', () => {
|
|
|
138
106
|
/>
|
|
139
107
|
);
|
|
140
108
|
|
|
141
|
-
// User should see the label
|
|
142
|
-
|
|
143
|
-
expect(
|
|
109
|
+
// User should see the label with an asterisk
|
|
110
|
+
const textInput = getByTestId('text-input');
|
|
111
|
+
expect(textInput).toBeTruthy();
|
|
144
112
|
|
|
145
113
|
// User should see all visual elements including custom components
|
|
146
|
-
expect(
|
|
147
|
-
expect(
|
|
148
|
-
expect(queryAllByTestId('text-input')).toHaveLength(1);
|
|
149
|
-
expect(queryAllByTestId('prefix-element')).toHaveLength(1);
|
|
114
|
+
expect(getByTestId('prefix-element')).toBeTruthy();
|
|
115
|
+
expect(getByTestId('suffix-element')).toBeTruthy();
|
|
150
116
|
|
|
151
117
|
expect(toJSON()).toMatchSnapshot();
|
|
152
118
|
});
|
|
@@ -154,7 +120,7 @@ describe('TextInput', () => {
|
|
|
154
120
|
|
|
155
121
|
describe('when user sees a required field', () => {
|
|
156
122
|
it('should indicate the field is required through styling', () => {
|
|
157
|
-
const { toJSON,
|
|
123
|
+
const { toJSON, getByTestId } = renderWithTheme(
|
|
158
124
|
<TextInput
|
|
159
125
|
label="Amount (AUD)"
|
|
160
126
|
prefix="dollar-sign"
|
|
@@ -163,13 +129,9 @@ describe('TextInput', () => {
|
|
|
163
129
|
/>
|
|
164
130
|
);
|
|
165
131
|
|
|
166
|
-
// User should see the label text
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
expect(queryAllByTestId('input-label')).toHaveLength(1);
|
|
170
|
-
expect(queryAllByTestId('input-suffix')).toHaveLength(1);
|
|
171
|
-
expect(queryAllByTestId('input-prefix')).toHaveLength(1);
|
|
172
|
-
expect(queryAllByTestId('text-input')).toHaveLength(1);
|
|
132
|
+
// User should see the label text with an asterisk
|
|
133
|
+
const textInput = getByTestId('text-input');
|
|
134
|
+
expect(textInput).toBeTruthy();
|
|
173
135
|
|
|
174
136
|
expect(toJSON()).toMatchSnapshot();
|
|
175
137
|
});
|
|
@@ -177,12 +139,7 @@ describe('TextInput', () => {
|
|
|
177
139
|
|
|
178
140
|
describe('when user has entered text', () => {
|
|
179
141
|
it('should show the input content and maintain all visual elements', () => {
|
|
180
|
-
const {
|
|
181
|
-
toJSON,
|
|
182
|
-
queryAllByTestId,
|
|
183
|
-
queryAllByText,
|
|
184
|
-
queryAllByDisplayValue,
|
|
185
|
-
} = renderWithTheme(
|
|
142
|
+
const { toJSON, getByDisplayValue } = renderWithTheme(
|
|
186
143
|
<TextInput
|
|
187
144
|
label="Amount (AUD)"
|
|
188
145
|
prefix="dollar-sign"
|
|
@@ -191,33 +148,28 @@ describe('TextInput', () => {
|
|
|
191
148
|
/>
|
|
192
149
|
);
|
|
193
150
|
|
|
194
|
-
// User should see the label with optional indicator
|
|
195
|
-
expect(queryAllByText('Amount (AUD) (Optional)')).toHaveLength(1);
|
|
196
|
-
|
|
197
151
|
// User should see their entered value
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// User should see all visual elements in active state
|
|
201
|
-
expect(queryAllByTestId('input-label')).toHaveLength(1);
|
|
202
|
-
expect(queryAllByTestId('input-prefix')).toHaveLength(1);
|
|
203
|
-
expect(queryAllByTestId('input-suffix')).toHaveLength(1);
|
|
204
|
-
expect(queryAllByTestId('text-input')).toHaveLength(1);
|
|
152
|
+
const textInput = getByDisplayValue('100');
|
|
153
|
+
expect(textInput).toBeTruthy();
|
|
205
154
|
|
|
206
155
|
expect(toJSON()).toMatchSnapshot();
|
|
207
156
|
});
|
|
208
157
|
});
|
|
209
158
|
|
|
159
|
+
describe('when label is not provided', () => {
|
|
160
|
+
it('should show the input content', () => {
|
|
161
|
+
const { getByDisplayValue } = renderWithTheme(
|
|
162
|
+
<TextInput value="This is the content" />
|
|
163
|
+
);
|
|
164
|
+
expect(getByDisplayValue('This is the content')).toBeTruthy();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
210
168
|
describe('when user encounters a read-only field', () => {
|
|
211
169
|
it('should display content but prevent user from editing', () => {
|
|
212
170
|
const onChangeText = jest.fn();
|
|
213
171
|
const onFocus = jest.fn();
|
|
214
|
-
const {
|
|
215
|
-
toJSON,
|
|
216
|
-
queryAllByTestId,
|
|
217
|
-
queryAllByText,
|
|
218
|
-
queryAllByDisplayValue,
|
|
219
|
-
getByTestId,
|
|
220
|
-
} = renderWithTheme(
|
|
172
|
+
const { toJSON, getByDisplayValue, getByTestId } = renderWithTheme(
|
|
221
173
|
<TextInput
|
|
222
174
|
label="Amount (AUD)"
|
|
223
175
|
prefix="dollar-sign"
|
|
@@ -226,40 +178,26 @@ describe('TextInput', () => {
|
|
|
226
178
|
value="Read-only value"
|
|
227
179
|
onChangeText={onChangeText}
|
|
228
180
|
onFocus={onFocus}
|
|
229
|
-
testID="readonly-input"
|
|
230
181
|
/>
|
|
231
182
|
);
|
|
232
183
|
|
|
233
184
|
// User should see the content and label
|
|
234
|
-
expect(queryAllByText('Amount (AUD) (Optional)')).toHaveLength(1);
|
|
235
|
-
expect(queryAllByDisplayValue('Read-only value')).toHaveLength(1);
|
|
236
|
-
|
|
237
|
-
// User should see all visual elements
|
|
238
|
-
expect(queryAllByTestId('input-label')).toHaveLength(1);
|
|
239
|
-
expect(queryAllByTestId('input-prefix')).toHaveLength(1);
|
|
240
|
-
expect(queryAllByTestId('input-suffix')).toHaveLength(1);
|
|
241
|
-
expect(queryAllByTestId('text-input')).toHaveLength(1);
|
|
242
|
-
|
|
243
|
-
// User should not be able to interact with the input due to Pressable being disabled
|
|
244
|
-
const inputContainer = getByTestId('readonly-input');
|
|
245
|
-
expect(inputContainer).toBeDisabled();
|
|
246
|
-
|
|
247
|
-
// User should see the input marked as read-only through accessibility
|
|
248
185
|
const textInput = getByTestId('text-input');
|
|
249
|
-
expect(textInput).
|
|
186
|
+
expect(textInput).toBeTruthy();
|
|
187
|
+
expect(getByDisplayValue('Read-only value')).toBeTruthy();
|
|
250
188
|
|
|
251
189
|
// The input should have editable=false prop
|
|
252
190
|
expect(textInput).toHaveProp('editable', false);
|
|
253
191
|
|
|
254
192
|
// Simulate user trying to type
|
|
255
193
|
fireEvent.changeText(textInput, 'New value');
|
|
194
|
+
expect(onChangeText).not.toHaveBeenCalled();
|
|
256
195
|
|
|
257
196
|
// It should still show the old value
|
|
258
197
|
expect(textInput.props.value).toBe('Read-only value');
|
|
259
198
|
|
|
260
199
|
// User should still see the original value displayed
|
|
261
|
-
expect(
|
|
262
|
-
expect(queryAllByDisplayValue('New value')).toHaveLength(0);
|
|
200
|
+
expect(getByDisplayValue('Read-only value')).toBeTruthy();
|
|
263
201
|
|
|
264
202
|
expect(toJSON()).toMatchSnapshot();
|
|
265
203
|
});
|
|
@@ -267,24 +205,18 @@ describe('TextInput', () => {
|
|
|
267
205
|
|
|
268
206
|
describe('when user sees a loading state', () => {
|
|
269
207
|
it('should show loading indicator in place of suffix icon', () => {
|
|
270
|
-
const { toJSON,
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
// User should see the label and content
|
|
282
|
-
expect(queryAllByText('Amount (AUD) (Optional)')).toHaveLength(1);
|
|
208
|
+
const { toJSON, getByTestId } = renderWithTheme(
|
|
209
|
+
<TextInput
|
|
210
|
+
label="Amount (AUD)"
|
|
211
|
+
prefix="dollar-sign"
|
|
212
|
+
suffix="arrow-down"
|
|
213
|
+
loading
|
|
214
|
+
value="100"
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
283
217
|
|
|
284
|
-
|
|
285
|
-
expect(
|
|
286
|
-
expect(queryAllByTestId('input-prefix')).toHaveLength(1);
|
|
287
|
-
expect(queryAllByTestId('text-input')).toHaveLength(1);
|
|
218
|
+
const textInput = getByTestId('text-input');
|
|
219
|
+
expect(textInput).toBeTruthy();
|
|
288
220
|
|
|
289
221
|
// User should see loading indicator instead of regular suffix
|
|
290
222
|
const suffixContainer = getByTestId('input-suffix');
|
|
@@ -301,60 +233,48 @@ describe('TextInput', () => {
|
|
|
301
233
|
|
|
302
234
|
describe('when user sees a textarea with character count', () => {
|
|
303
235
|
it('should display multiline input with character counter', () => {
|
|
304
|
-
const { toJSON,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
);
|
|
236
|
+
const { toJSON, getByTestId, getByText } = renderWithTheme(
|
|
237
|
+
<TextInput
|
|
238
|
+
label="Amount (AUD)"
|
|
239
|
+
prefix="dollar-sign"
|
|
240
|
+
suffix="arrow-down"
|
|
241
|
+
value="100"
|
|
242
|
+
maxLength={255}
|
|
243
|
+
variant="textarea"
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
315
246
|
|
|
316
247
|
// User should see the label and character count
|
|
317
|
-
|
|
318
|
-
expect(
|
|
319
|
-
|
|
320
|
-
// User should see all visual elements
|
|
321
|
-
expect(queryAllByTestId('input-label')).toHaveLength(1);
|
|
322
|
-
expect(queryAllByTestId('input-prefix')).toHaveLength(1);
|
|
323
|
-
expect(queryAllByTestId('input-suffix')).toHaveLength(1);
|
|
324
|
-
expect(queryAllByTestId('text-input')).toHaveLength(1);
|
|
248
|
+
const textInput = getByTestId('text-input');
|
|
249
|
+
expect(textInput).toBeTruthy();
|
|
250
|
+
expect(getByText('3/255')).toBeTruthy();
|
|
325
251
|
|
|
326
252
|
// User should be able to enter multiple lines
|
|
327
|
-
expect(
|
|
253
|
+
expect(textInput).toHaveProp('multiline', true);
|
|
328
254
|
|
|
329
255
|
expect(toJSON()).toMatchSnapshot();
|
|
330
256
|
});
|
|
331
257
|
|
|
332
258
|
it('should hide character count when user requests it', () => {
|
|
333
|
-
const { toJSON,
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
);
|
|
259
|
+
const { toJSON, queryByText, getByTestId } = renderWithTheme(
|
|
260
|
+
<TextInput
|
|
261
|
+
label="Amount (AUD)"
|
|
262
|
+
prefix="dollar-sign"
|
|
263
|
+
suffix="arrow-down"
|
|
264
|
+
value="100"
|
|
265
|
+
maxLength={255}
|
|
266
|
+
hideCharacterCount
|
|
267
|
+
variant="textarea"
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
345
270
|
|
|
346
271
|
// User should see the label but not the character count
|
|
347
|
-
|
|
348
|
-
expect(
|
|
349
|
-
|
|
350
|
-
// User should see all visual elements
|
|
351
|
-
expect(queryAllByTestId('input-label')).toHaveLength(1);
|
|
352
|
-
expect(queryAllByTestId('input-prefix')).toHaveLength(1);
|
|
353
|
-
expect(queryAllByTestId('input-suffix')).toHaveLength(1);
|
|
354
|
-
expect(queryAllByTestId('text-input')).toHaveLength(1);
|
|
272
|
+
const textInput = getByTestId('text-input');
|
|
273
|
+
expect(textInput).toBeTruthy();
|
|
274
|
+
expect(queryByText('3/255')).toBeFalsy();
|
|
355
275
|
|
|
356
276
|
// User should still be able to enter multiple lines
|
|
357
|
-
expect(
|
|
277
|
+
expect(textInput).toHaveProp('multiline', true);
|
|
358
278
|
|
|
359
279
|
expect(toJSON()).toMatchSnapshot();
|
|
360
280
|
});
|
|
@@ -363,25 +283,10 @@ describe('TextInput', () => {
|
|
|
363
283
|
describe('when user encounters a disabled field', () => {
|
|
364
284
|
it('should display content but prevent any user interaction', () => {
|
|
365
285
|
const { toJSON, getByTestId } = renderWithTheme(
|
|
366
|
-
<TextInput
|
|
367
|
-
label="Amount (AUD)"
|
|
368
|
-
required
|
|
369
|
-
disabled
|
|
370
|
-
value="100"
|
|
371
|
-
testID="disabled-text-input"
|
|
372
|
-
/>
|
|
286
|
+
<TextInput label="Amount (AUD)" disabled value="100" />
|
|
373
287
|
);
|
|
374
288
|
|
|
375
|
-
const
|
|
376
|
-
const textInput = within(disabledInput).getByTestId('text-input');
|
|
377
|
-
const inputLabel = within(disabledInput).getByTestId('input-label');
|
|
378
|
-
|
|
379
|
-
// User should see the structure but understand it's disabled
|
|
380
|
-
expect(disabledInput).toBeTruthy();
|
|
381
|
-
expect(textInput).toBeTruthy();
|
|
382
|
-
expect(inputLabel).toBeTruthy();
|
|
383
|
-
|
|
384
|
-
// User should not be able to interact with the input
|
|
289
|
+
const textInput = getByTestId('text-input');
|
|
385
290
|
expect(textInput).toBeDisabled();
|
|
386
291
|
|
|
387
292
|
// Visual styling should indicate disabled state
|
|
@@ -413,7 +318,7 @@ describe('TextInput', () => {
|
|
|
413
318
|
|
|
414
319
|
describe('when user sees an error state', () => {
|
|
415
320
|
it('should display error message to help user understand the issue', () => {
|
|
416
|
-
const { toJSON,
|
|
321
|
+
const { toJSON, getByText } = renderWithTheme(
|
|
417
322
|
<TextInput
|
|
418
323
|
label="Amount (AUD)"
|
|
419
324
|
prefix="dollar-sign"
|
|
@@ -423,8 +328,7 @@ describe('TextInput', () => {
|
|
|
423
328
|
);
|
|
424
329
|
|
|
425
330
|
// User should see both the label and error message
|
|
426
|
-
expect(
|
|
427
|
-
expect(queryAllByText('This field is required')).toHaveLength(1);
|
|
331
|
+
expect(getByText('This field is required')).toBeTruthy();
|
|
428
332
|
|
|
429
333
|
expect(toJSON()).toMatchSnapshot();
|
|
430
334
|
});
|
|
@@ -432,7 +336,7 @@ describe('TextInput', () => {
|
|
|
432
336
|
|
|
433
337
|
describe('when user sees helper text', () => {
|
|
434
338
|
it('should display guidance text to assist user understanding', () => {
|
|
435
|
-
const { toJSON,
|
|
339
|
+
const { toJSON, getByText } = renderWithTheme(
|
|
436
340
|
<TextInput
|
|
437
341
|
label="Amount (AUD)"
|
|
438
342
|
prefix="dollar-sign"
|
|
@@ -442,8 +346,7 @@ describe('TextInput', () => {
|
|
|
442
346
|
);
|
|
443
347
|
|
|
444
348
|
// User should see both the label and helpful guidance
|
|
445
|
-
expect(
|
|
446
|
-
expect(queryAllByText('This is helper text')).toHaveLength(1);
|
|
349
|
+
expect(getByText('This is helper text')).toBeTruthy();
|
|
447
350
|
|
|
448
351
|
expect(toJSON()).toMatchSnapshot();
|
|
449
352
|
});
|
|
@@ -451,8 +354,8 @@ describe('TextInput', () => {
|
|
|
451
354
|
|
|
452
355
|
describe('when user interacts with placeholder text', () => {
|
|
453
356
|
describe('starting from empty field', () => {
|
|
454
|
-
it('should show placeholder when user focuses, hide when user leaves empty', () => {
|
|
455
|
-
const
|
|
357
|
+
it('should show placeholder when user focuses, hide when user leaves empty', async () => {
|
|
358
|
+
const { getByTestId, queryByPlaceholderText, toJSON } = renderWithTheme(
|
|
456
359
|
<TextInput
|
|
457
360
|
label="Amount (AUD)"
|
|
458
361
|
prefix="dollar-sign"
|
|
@@ -462,21 +365,25 @@ describe('TextInput', () => {
|
|
|
462
365
|
/>
|
|
463
366
|
);
|
|
464
367
|
|
|
465
|
-
|
|
466
|
-
expect(
|
|
368
|
+
const textInput = getByTestId('text-input');
|
|
369
|
+
expect(textInput).toBeTruthy();
|
|
467
370
|
|
|
468
371
|
// User focuses the input field
|
|
469
|
-
fireEvent(
|
|
470
|
-
|
|
372
|
+
fireEvent(textInput, 'focus');
|
|
373
|
+
await waitFor(() => {
|
|
374
|
+
expect(queryByPlaceholderText('Enter Amount')).toBeTruthy();
|
|
375
|
+
});
|
|
471
376
|
|
|
472
377
|
// User leaves the field empty and moves focus away
|
|
473
|
-
fireEvent(
|
|
474
|
-
|
|
475
|
-
'placeholder',
|
|
476
|
-
|
|
477
|
-
);
|
|
378
|
+
fireEvent(textInput, 'blur');
|
|
379
|
+
await waitFor(() => {
|
|
380
|
+
expect(textInput).toHaveProp('placeholder', ' ');
|
|
381
|
+
});
|
|
478
382
|
|
|
479
|
-
|
|
383
|
+
// Input remains empty, so placeholder should be hidden again
|
|
384
|
+
expect(queryByPlaceholderText('Placeholder')).toBeFalsy();
|
|
385
|
+
|
|
386
|
+
expect(toJSON()).toMatchSnapshot();
|
|
480
387
|
});
|
|
481
388
|
});
|
|
482
389
|
});
|
|
@@ -484,7 +391,7 @@ describe('TextInput', () => {
|
|
|
484
391
|
describe('when user provides default values', () => {
|
|
485
392
|
describe('starting with pre-filled content', () => {
|
|
486
393
|
it('should display default value and character count', () => {
|
|
487
|
-
const
|
|
394
|
+
const { getByDisplayValue, getByText, toJSON } = renderWithTheme(
|
|
488
395
|
<TextInput
|
|
489
396
|
label="Amount (AUD)"
|
|
490
397
|
prefix="dollar-sign"
|
|
@@ -497,45 +404,46 @@ describe('TextInput', () => {
|
|
|
497
404
|
);
|
|
498
405
|
|
|
499
406
|
// User should see the pre-filled value
|
|
500
|
-
expect(
|
|
407
|
+
expect(getByDisplayValue('1000')).toBeTruthy();
|
|
501
408
|
|
|
502
409
|
// User should see character count reflecting the default value
|
|
503
|
-
expect(
|
|
410
|
+
expect(getByText('4/255')).toBeTruthy();
|
|
504
411
|
|
|
505
|
-
expect(
|
|
412
|
+
expect(toJSON()).toMatchSnapshot();
|
|
506
413
|
});
|
|
507
414
|
});
|
|
508
415
|
|
|
509
416
|
describe('when both default and controlled values are provided', () => {
|
|
510
417
|
it('should prioritize controlled value over default value', () => {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
418
|
+
const { getByDisplayValue, queryByDisplayValue, getByText, toJSON } =
|
|
419
|
+
renderWithTheme(
|
|
420
|
+
<TextInput
|
|
421
|
+
label="Amount (AUD)"
|
|
422
|
+
prefix="dollar-sign"
|
|
423
|
+
required
|
|
424
|
+
helpText="This is helper text"
|
|
425
|
+
placeholder="Enter Amount"
|
|
426
|
+
defaultValue="1000"
|
|
427
|
+
value="2000"
|
|
428
|
+
maxLength={255}
|
|
429
|
+
/>
|
|
430
|
+
);
|
|
523
431
|
|
|
524
432
|
// User should see the controlled value, not the default
|
|
525
|
-
expect(
|
|
526
|
-
expect(
|
|
433
|
+
expect(getByDisplayValue('2000')).toBeVisible();
|
|
434
|
+
expect(queryByDisplayValue('1000')).toBeFalsy();
|
|
527
435
|
|
|
528
436
|
// Character count should reflect the actual displayed value
|
|
529
|
-
expect(
|
|
437
|
+
expect(getByText('4/255')).toBeTruthy();
|
|
530
438
|
|
|
531
|
-
expect(
|
|
439
|
+
expect(toJSON()).toMatchSnapshot();
|
|
532
440
|
});
|
|
533
441
|
});
|
|
534
442
|
});
|
|
535
443
|
|
|
536
444
|
describe('when user applies custom styling', () => {
|
|
537
445
|
it('should respect user-provided background color styling', () => {
|
|
538
|
-
const
|
|
446
|
+
const { getByTestId, toJSON } = renderWithTheme(
|
|
539
447
|
<TextInput
|
|
540
448
|
label="Amount (AUD)"
|
|
541
449
|
prefix="dollar-sign"
|
|
@@ -549,6 +457,8 @@ describe('TextInput', () => {
|
|
|
549
457
|
/>
|
|
550
458
|
);
|
|
551
459
|
|
|
460
|
+
const textInput = getByTestId('text-input');
|
|
461
|
+
|
|
552
462
|
// Helper function to extract background color from complex React Native styles
|
|
553
463
|
function getBackgroundColor(
|
|
554
464
|
style: StyleProp<ViewStyle> | unknown[]
|
|
@@ -578,13 +488,13 @@ describe('TextInput', () => {
|
|
|
578
488
|
}
|
|
579
489
|
|
|
580
490
|
// User should see their custom background color applied
|
|
581
|
-
const textInputStyle =
|
|
491
|
+
const textInputStyle = textInput.props.style;
|
|
582
492
|
expect(getBackgroundColor(textInputStyle)).toBe('customColor');
|
|
583
493
|
|
|
584
|
-
const borderStyle =
|
|
494
|
+
const borderStyle = getByTestId('text-input-border').props.style;
|
|
585
495
|
expect(getBackgroundColor(borderStyle)).toBe('customColor');
|
|
586
496
|
|
|
587
|
-
expect(
|
|
497
|
+
expect(toJSON()).toMatchSnapshot();
|
|
588
498
|
});
|
|
589
499
|
});
|
|
590
500
|
|
|
@@ -596,13 +506,12 @@ describe('TextInput', () => {
|
|
|
596
506
|
}
|
|
597
507
|
>();
|
|
598
508
|
|
|
599
|
-
const
|
|
509
|
+
const { getByDisplayValue, toJSON } = renderWithTheme(
|
|
600
510
|
<TextInput label="Amount (AUD)" value="2000" ref={ref} />
|
|
601
511
|
);
|
|
602
512
|
|
|
603
513
|
// User should see the initial value
|
|
604
|
-
expect(
|
|
605
|
-
expect(wrapper.queryByDisplayValue('1000')).toBeFalsy();
|
|
514
|
+
expect(getByDisplayValue('2000')).toBeTruthy();
|
|
606
515
|
|
|
607
516
|
// External code should be able to control the input programmatically
|
|
608
517
|
const nativeTextInputRef = ref.current?.getNativeTextInputRef();
|
|
@@ -629,21 +538,18 @@ describe('TextInput', () => {
|
|
|
629
538
|
expect(blurSpy).toHaveBeenCalledTimes(1);
|
|
630
539
|
}
|
|
631
540
|
|
|
632
|
-
expect(
|
|
541
|
+
expect(toJSON()).toMatchSnapshot();
|
|
633
542
|
});
|
|
634
543
|
});
|
|
635
544
|
|
|
636
545
|
describe('when user chooses textarea variant', () => {
|
|
637
546
|
it('should provide multiline text input experience', () => {
|
|
638
|
-
const { toJSON,
|
|
547
|
+
const { toJSON, getByDisplayValue } = renderWithTheme(
|
|
639
548
|
<TextInput label="Amount (AUD)" value="2000" variant="textarea" />
|
|
640
549
|
);
|
|
641
550
|
|
|
642
|
-
// User should see the label with optional indicator
|
|
643
|
-
expect(queryByText('Amount (AUD) (Optional)')).toBeTruthy();
|
|
644
|
-
|
|
645
551
|
// User should see their entered value
|
|
646
|
-
expect(
|
|
552
|
+
expect(getByDisplayValue('2000')).toBeTruthy();
|
|
647
553
|
|
|
648
554
|
expect(toJSON()).toMatchSnapshot();
|
|
649
555
|
});
|
|
@@ -652,15 +558,8 @@ describe('TextInput', () => {
|
|
|
652
558
|
describe('when user encounters an error state that needs to be corrected', () => {
|
|
653
559
|
it('should allow focusing input even when error is present', () => {
|
|
654
560
|
const onFocus = jest.fn();
|
|
655
|
-
const onBlur = jest.fn();
|
|
656
561
|
const { getByTestId } = renderWithTheme(
|
|
657
|
-
<TextInput
|
|
658
|
-
label="Email"
|
|
659
|
-
error="Please enter a valid email address"
|
|
660
|
-
onFocus={onFocus}
|
|
661
|
-
onBlur={onBlur}
|
|
662
|
-
testID="error-input"
|
|
663
|
-
/>
|
|
562
|
+
<TextInput label="Email" error="Invalid Email" onFocus={onFocus} />
|
|
664
563
|
);
|
|
665
564
|
|
|
666
565
|
// User should be able to focus the input despite the error
|
|
@@ -668,32 +567,112 @@ describe('TextInput', () => {
|
|
|
668
567
|
fireEvent(textInput, 'focus');
|
|
669
568
|
|
|
670
569
|
// Should trigger focus callback
|
|
671
|
-
expect(onFocus).
|
|
672
|
-
|
|
673
|
-
// User should be able to blur the input
|
|
674
|
-
fireEvent(textInput, 'blur');
|
|
675
|
-
expect(onBlur).toHaveBeenCalledTimes(1);
|
|
570
|
+
expect(onFocus).toHaveBeenCalled();
|
|
676
571
|
});
|
|
677
572
|
|
|
678
|
-
it('should display focused styling when error input is focused', () => {
|
|
573
|
+
it('should display focused styling when error input is focused', async () => {
|
|
679
574
|
const { getByTestId } = renderWithTheme(
|
|
680
|
-
<TextInput
|
|
681
|
-
label="Email"
|
|
682
|
-
error="Please enter a valid email address"
|
|
683
|
-
testID="error-input"
|
|
684
|
-
/>
|
|
575
|
+
<TextInput label="Email" error="Invalid Email" />
|
|
685
576
|
);
|
|
686
577
|
|
|
687
|
-
// Get the border element
|
|
688
|
-
const border = getByTestId('text-input-border');
|
|
689
|
-
|
|
690
578
|
// Focus the input
|
|
691
579
|
const textInput = getByTestId('text-input');
|
|
692
580
|
fireEvent(textInput, 'focus');
|
|
693
581
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
582
|
+
await waitFor(() => {
|
|
583
|
+
const border = getByTestId('text-input-border');
|
|
584
|
+
expect(border).toHaveProp('themeFocused', true);
|
|
585
|
+
expect(border).toHaveProp('themeState', 'error');
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
describe('when text is changed in the input', () => {
|
|
591
|
+
it('updates the display value', () => {
|
|
592
|
+
const ControllableInput = () => {
|
|
593
|
+
const [value, setValue] = React.useState('');
|
|
594
|
+
return (
|
|
595
|
+
<TextInput
|
|
596
|
+
label="Amount (AUD)"
|
|
597
|
+
value={value}
|
|
598
|
+
onChangeText={setValue}
|
|
599
|
+
/>
|
|
600
|
+
);
|
|
601
|
+
};
|
|
602
|
+
const { getByTestId, getByDisplayValue } = renderWithTheme(
|
|
603
|
+
<ControllableInput />
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
fireEvent.changeText(getByTestId('text-input'), '100');
|
|
607
|
+
expect(getByDisplayValue('100')).toBeTruthy();
|
|
697
608
|
});
|
|
609
|
+
|
|
610
|
+
it('does not update when not editable', () => {
|
|
611
|
+
const ControllableInput = () => {
|
|
612
|
+
const [value, setValue] = React.useState('hello');
|
|
613
|
+
return (
|
|
614
|
+
<TextInput
|
|
615
|
+
label="Amount (AUD)"
|
|
616
|
+
value={value}
|
|
617
|
+
onChangeText={setValue}
|
|
618
|
+
editable={false}
|
|
619
|
+
/>
|
|
620
|
+
);
|
|
621
|
+
};
|
|
622
|
+
const { getByTestId, getByDisplayValue } = renderWithTheme(
|
|
623
|
+
<ControllableInput />
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
// This should not work
|
|
627
|
+
fireEvent.changeText(getByTestId('text-input'), 'new value');
|
|
628
|
+
|
|
629
|
+
// show old value
|
|
630
|
+
expect(getByDisplayValue('hello')).toBeTruthy();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('allows user to press label to focus and type', () => {
|
|
634
|
+
const ControllableInput = () => {
|
|
635
|
+
const [value, setValue] = React.useState('');
|
|
636
|
+
return (
|
|
637
|
+
<TextInput label="My Label" value={value} onChangeText={setValue} />
|
|
638
|
+
);
|
|
639
|
+
};
|
|
640
|
+
const { getByText, getByDisplayValue, getByTestId } = renderWithTheme(
|
|
641
|
+
<ControllableInput />
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
// Pressing the label should focus the input
|
|
645
|
+
fireEvent.press(getByText('My Label'));
|
|
646
|
+
|
|
647
|
+
// Now we can type into the focused input
|
|
648
|
+
fireEvent.changeText(getByTestId('text-input'), 'hello');
|
|
649
|
+
|
|
650
|
+
// Verify the value has changed
|
|
651
|
+
expect(getByDisplayValue('hello')).toBeVisible();
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should allow user to type text', () => {
|
|
656
|
+
const handleChangeText = jest.fn();
|
|
657
|
+
const { getByTestId } = renderWithTheme(
|
|
658
|
+
<TextInput
|
|
659
|
+
label="Email"
|
|
660
|
+
onChangeText={handleChangeText}
|
|
661
|
+
testID="email-input-container"
|
|
662
|
+
/>
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
// 1. Get both the container and the actual input field
|
|
666
|
+
const container = getByTestId('email-input-container');
|
|
667
|
+
const textInput = getByTestId('text-input');
|
|
668
|
+
|
|
669
|
+
// 2. Focus the component by interacting with the container
|
|
670
|
+
fireEvent(container, 'focus');
|
|
671
|
+
|
|
672
|
+
// 3. Type text by firing changeText on the input field itself
|
|
673
|
+
fireEvent.changeText(textInput, 'hello world');
|
|
674
|
+
|
|
675
|
+
// 4. Assert that your onChangeText handler was called with the correct text
|
|
676
|
+
expect(handleChangeText).toHaveBeenCalledWith('hello world');
|
|
698
677
|
});
|
|
699
678
|
});
|