@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.
@@ -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
- // User should see the main input container
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
- // User should see the label text with optional indicator
46
- expect(
47
- within(inputContainer).getByText('Amount (AUD) (Optional)')
48
- ).toBeTruthy();
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
- // For accessibility: input content should be hidden from screen readers in idle state
51
- expect(inputRowWrapper).toHaveProp('accessibilityElementsHidden', true);
52
- expect(inputPrefix).toHaveProp('accessibilityElementsHidden', true);
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 = within(inputContainer).getByA11yLabel(
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 { getByTestId } = renderWithTheme(
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
- within(inputContainer).queryAllByTestId('text-input')
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 inputContainer = getByTestId('idle-text-input');
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 clicks on the label to focus the input
114
- fireEvent.press(labelElement);
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, queryAllByTestId, queryAllByText } = renderWithTheme(
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 without asterisk (since required styling is handled differently)
142
- expect(queryAllByText('Amount (AUD)')).toHaveLength(1);
143
- expect(queryAllByText('*')).toHaveLength(0);
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(queryAllByTestId('input-label')).toHaveLength(1);
147
- expect(queryAllByTestId('suffix-element')).toHaveLength(1);
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, queryAllByTestId, queryAllByText } = renderWithTheme(
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
- expect(queryAllByText('Amount (AUD)')).toHaveLength(1);
168
- // User should see all input elements
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
- expect(queryAllByDisplayValue('100')).toHaveLength(1);
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).toBeDisabled();
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(queryAllByDisplayValue('Read-only value')).toHaveLength(1);
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, queryAllByTestId, queryAllByText, getByTestId } =
271
- renderWithTheme(
272
- <TextInput
273
- label="Amount (AUD)"
274
- prefix="dollar-sign"
275
- suffix="arrow-down"
276
- loading
277
- value="100"
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
- // User should see all input elements
285
- expect(queryAllByTestId('input-label')).toHaveLength(1);
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, queryAllByTestId, queryAllByText, getByTestId } =
305
- renderWithTheme(
306
- <TextInput
307
- label="Amount (AUD)"
308
- prefix="dollar-sign"
309
- suffix="arrow-down"
310
- value="100"
311
- maxLength={255}
312
- variant="textarea"
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
- expect(queryAllByText('Amount (AUD) (Optional)')).toHaveLength(1);
318
- expect(queryAllByText('3/255')).toHaveLength(1);
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(getByTestId('text-input')).toHaveProp('multiline', true);
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, queryAllByTestId, queryAllByText, getByTestId } =
334
- renderWithTheme(
335
- <TextInput
336
- label="Amount (AUD)"
337
- prefix="dollar-sign"
338
- suffix="arrow-down"
339
- value="100"
340
- maxLength={255}
341
- variant="textarea"
342
- hideCharacterCount
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
- expect(queryAllByText('Amount (AUD) (Optional)')).toHaveLength(1);
348
- expect(queryAllByText('3/255')).toHaveLength(0);
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(getByTestId('text-input')).toHaveProp('multiline', true);
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 disabledInput = getByTestId('disabled-text-input');
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, queryAllByText } = renderWithTheme(
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(queryAllByText('Amount (AUD)')).toHaveLength(1);
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, queryAllByText } = renderWithTheme(
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(queryAllByText('Amount (AUD)')).toHaveLength(1);
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 wrapper = renderWithTheme(
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
- // User should see the input field
466
- expect(wrapper.queryByTestId('text-input')).toBeTruthy();
368
+ const textInput = getByTestId('text-input');
369
+ expect(textInput).toBeTruthy();
467
370
 
468
371
  // User focuses the input field
469
- fireEvent(wrapper.getByTestId('text-input'), 'focus');
470
- expect(wrapper.queryByPlaceholderText('Enter Amount')).toBeTruthy();
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(wrapper.getByTestId('text-input'), 'blur');
474
- expect(wrapper.getByTestId('text-input')).toHaveProp(
475
- 'placeholder',
476
- ' '
477
- );
378
+ fireEvent(textInput, 'blur');
379
+ await waitFor(() => {
380
+ expect(textInput).toHaveProp('placeholder', ' ');
381
+ });
478
382
 
479
- expect(wrapper.toJSON()).toMatchSnapshot();
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 wrapper = renderWithTheme(
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(wrapper.queryByDisplayValue('1000')).toBeTruthy();
407
+ expect(getByDisplayValue('1000')).toBeTruthy();
501
408
 
502
409
  // User should see character count reflecting the default value
503
- expect(wrapper.queryByText('4/255')).toBeTruthy();
410
+ expect(getByText('4/255')).toBeTruthy();
504
411
 
505
- expect(wrapper.toJSON()).toMatchSnapshot();
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 wrapper = renderWithTheme(
512
- <TextInput
513
- label="Amount (AUD)"
514
- prefix="dollar-sign"
515
- required
516
- helpText="This is helper text"
517
- placeholder="Enter Amount"
518
- defaultValue="1000"
519
- value="2000"
520
- maxLength={255}
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(wrapper.queryByDisplayValue('2000')).toBeTruthy();
526
- expect(wrapper.queryByDisplayValue('1000')).toBeFalsy();
433
+ expect(getByDisplayValue('2000')).toBeVisible();
434
+ expect(queryByDisplayValue('1000')).toBeFalsy();
527
435
 
528
436
  // Character count should reflect the actual displayed value
529
- expect(wrapper.queryByText('4/255')).toBeTruthy();
437
+ expect(getByText('4/255')).toBeTruthy();
530
438
 
531
- expect(wrapper.toJSON()).toMatchSnapshot();
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 wrapper = renderWithTheme(
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 = wrapper.getByTestId('text-input').props.style;
491
+ const textInputStyle = textInput.props.style;
582
492
  expect(getBackgroundColor(textInputStyle)).toBe('customColor');
583
493
 
584
- const borderStyle = wrapper.getByTestId('text-input-border').props.style;
494
+ const borderStyle = getByTestId('text-input-border').props.style;
585
495
  expect(getBackgroundColor(borderStyle)).toBe('customColor');
586
496
 
587
- expect(wrapper.toJSON()).toMatchSnapshot();
497
+ expect(toJSON()).toMatchSnapshot();
588
498
  });
589
499
  });
590
500
 
@@ -596,13 +506,12 @@ describe('TextInput', () => {
596
506
  }
597
507
  >();
598
508
 
599
- const wrapper = renderWithTheme(
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(wrapper.queryByDisplayValue('2000')).toBeTruthy();
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(wrapper.toJSON()).toMatchSnapshot();
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, queryByText, queryByDisplayValue } = renderWithTheme(
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(queryByDisplayValue('2000')).toBeTruthy();
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).toHaveBeenCalledTimes(1);
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
- // Border should have focused styling (themeFocused=true)
695
- expect(border).toHaveProp('themeFocused', true);
696
- expect(border).toHaveProp('themeState', 'error');
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
  });