@hero-design/rn-work-uikit 1.1.0-alpha.0 → 1.2.0-alpha.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/.cursorrules +57 -0
- package/CHANGELOG.md +8 -3
- package/DEVELOPMENT.md +118 -0
- package/THEME_OVERRIDE.md +52 -0
- package/eslint.config.js +20 -0
- package/lib/index.js +1000 -4
- package/locales/en_AU.js +10 -0
- package/locales/en_AU.mjs +8 -0
- package/locales/en_CA.js +10 -0
- package/locales/en_CA.mjs +8 -0
- package/locales/index.js +11 -0
- package/locales/index.mjs +9 -0
- package/locales/types.js +2 -0
- package/locales/types.mjs +1 -0
- package/package.json +8 -4
- package/rollup.config.mjs +18 -2
- package/src/__tests__/__snapshots__/index.spec.tsx.snap +91 -116
- package/src/__tests__/index.spec.tsx +15 -0
- package/src/__tests__/theme-export-override.spec.ts +96 -0
- package/src/components/TextInput/ErrorOrHelpText.tsx +58 -0
- package/src/components/TextInput/FloatingLabel.tsx +120 -0
- package/src/components/TextInput/InputComponent.tsx +61 -0
- package/src/components/TextInput/InputRow.tsx +103 -0
- package/src/components/TextInput/MaxLengthMessage.tsx +66 -0
- package/src/components/TextInput/PrefixComponent.tsx +77 -0
- package/src/components/TextInput/StyledTextInput.tsx +134 -0
- package/src/components/TextInput/SuffixComponent.tsx +73 -0
- package/src/components/TextInput/__tests__/ErrorOrHelpText.spec.tsx +20 -0
- package/src/components/TextInput/__tests__/FloatingLabel.spec.tsx +203 -0
- package/src/components/TextInput/__tests__/InputComponent.spec.tsx +39 -0
- package/src/components/TextInput/__tests__/InputRow.spec.tsx +275 -0
- package/src/components/TextInput/__tests__/MaxLengthMessage.spec.tsx +17 -0
- package/src/components/TextInput/__tests__/PrefixComponent.spec.tsx +14 -0
- package/src/components/TextInput/__tests__/StyledTextInput.spec.tsx +114 -0
- package/src/components/TextInput/__tests__/SuffixComponent.spec.tsx +20 -0
- package/src/components/TextInput/__tests__/__snapshots__/StyledTextInput.spec.tsx.snap +571 -0
- package/src/components/TextInput/__tests__/__snapshots__/index.spec.tsx.snap +5671 -0
- package/src/components/TextInput/__tests__/getState.spec.tsx +89 -0
- package/src/components/TextInput/__tests__/index.spec.tsx +699 -0
- package/src/components/TextInput/constants.ts +1 -0
- package/src/components/TextInput/index.tsx +327 -0
- package/src/components/TextInput/types.ts +95 -0
- package/src/emotion.d.ts +15 -0
- package/src/index.ts +16 -1
- package/src/jest.d.ts +24 -0
- package/src/theme/ThemeProvider.ts +20 -0
- package/src/theme/ThemeSwitcher.tsx +76 -0
- package/src/theme/__tests__/ThemeProvider.spec.tsx +32 -0
- package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +1851 -0
- package/src/theme/__tests__/index.spec.ts +7 -0
- package/src/theme/components/textInput.ts +92 -0
- package/src/theme/getTheme.ts +32 -0
- package/src/theme/index.ts +17 -0
- package/src/utils/__tests__/helpers.spec.ts +92 -0
- package/src/utils/helpers.ts +113 -0
- package/testUtils/renderWithTheme.tsx +6 -3
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderWithTheme from '../../../../testUtils/renderWithTheme';
|
|
3
|
+
import FloatingLabel from '../FloatingLabel';
|
|
4
|
+
|
|
5
|
+
describe('FloatingLabel', () => {
|
|
6
|
+
describe('label text display based on required prop', () => {
|
|
7
|
+
it.each`
|
|
8
|
+
required | expectedText | shouldShowOptional | description
|
|
9
|
+
${true} | ${'Email'} | ${false} | ${'required field without optional indicator'}
|
|
10
|
+
${false} | ${'Phone (Optional)'} | ${true} | ${'optional field with optional indicator'}
|
|
11
|
+
`(
|
|
12
|
+
'should display $expectedText for $description',
|
|
13
|
+
({ required, expectedText, shouldShowOptional }) => {
|
|
14
|
+
const label = required ? 'Email' : 'Phone';
|
|
15
|
+
const { getByText, queryByText } = renderWithTheme(
|
|
16
|
+
<FloatingLabel
|
|
17
|
+
label={label}
|
|
18
|
+
variant="text"
|
|
19
|
+
state="default"
|
|
20
|
+
isFocused={false}
|
|
21
|
+
required={required}
|
|
22
|
+
isEmptyValue
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// User should see the appropriate label text
|
|
27
|
+
expect(getByText(expectedText)).toBeTruthy();
|
|
28
|
+
|
|
29
|
+
if (!shouldShowOptional) {
|
|
30
|
+
expect(queryByText(`${label} (Optional)`)).toBeFalsy();
|
|
31
|
+
expect(queryByText('(Optional)')).toBeFalsy();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('label behavior based on focus and content state', () => {
|
|
38
|
+
it.each`
|
|
39
|
+
state | isEmptyValue | expectedBehavior | description
|
|
40
|
+
${'focused'} | ${true} | ${'label in focused state'} | ${'user focuses empty input'}
|
|
41
|
+
${'filled'} | ${false} | ${'label remains in floating position'} | ${'user enters text in input'}
|
|
42
|
+
`(
|
|
43
|
+
'should show $expectedBehavior when $description',
|
|
44
|
+
({ state, isEmptyValue }) => {
|
|
45
|
+
const { getByTestId } = renderWithTheme(
|
|
46
|
+
<FloatingLabel
|
|
47
|
+
label={state === 'focused' ? 'Password' : 'Message'}
|
|
48
|
+
variant="text"
|
|
49
|
+
state={state}
|
|
50
|
+
isFocused={state === 'focused'}
|
|
51
|
+
required
|
|
52
|
+
isEmptyValue={isEmptyValue}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// User should see the label behaving appropriately
|
|
57
|
+
const label = getByTestId('input-label');
|
|
58
|
+
expect(label).toBeTruthy();
|
|
59
|
+
const { children } = label.props;
|
|
60
|
+
const expectedText = state === 'focused' ? 'Password' : 'Message';
|
|
61
|
+
expect(
|
|
62
|
+
Array.isArray(children) ? children.filter(Boolean).join('') : children
|
|
63
|
+
).toBe(expectedText);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('variant-specific positioning', () => {
|
|
69
|
+
it.each`
|
|
70
|
+
variant | required | expectedText | description
|
|
71
|
+
${'textarea'} | ${false} | ${'Description (Optional)'} | ${'multiline input with optional label'}
|
|
72
|
+
`(
|
|
73
|
+
'should position label appropriately for $variant ($description)',
|
|
74
|
+
({ variant, required, expectedText }) => {
|
|
75
|
+
const { getByTestId } = renderWithTheme(
|
|
76
|
+
<FloatingLabel
|
|
77
|
+
label="Description"
|
|
78
|
+
variant={variant}
|
|
79
|
+
state="default"
|
|
80
|
+
isFocused={false}
|
|
81
|
+
required={required}
|
|
82
|
+
isEmptyValue
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// User should see the label with proper variant positioning
|
|
87
|
+
const label = getByTestId('input-label');
|
|
88
|
+
expect(label).toBeTruthy();
|
|
89
|
+
const { children } = label.props;
|
|
90
|
+
expect(Array.isArray(children) ? children.join('') : children).toBe(
|
|
91
|
+
expectedText
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('when user encounters different input states', () => {
|
|
98
|
+
it.each`
|
|
99
|
+
state | required | expectedText | description
|
|
100
|
+
${'error'} | ${true} | ${'Email'} | ${'validation error with required field'}
|
|
101
|
+
${'disabled'} | ${false} | ${'Disabled Field (Optional)'} | ${'non-interactive optional field'}
|
|
102
|
+
`(
|
|
103
|
+
'should display appropriate styling for $state state ($description)',
|
|
104
|
+
({ state, required, expectedText }) => {
|
|
105
|
+
const { getByTestId } = renderWithTheme(
|
|
106
|
+
<FloatingLabel
|
|
107
|
+
label={state === 'error' ? 'Email' : 'Disabled Field'}
|
|
108
|
+
variant="text"
|
|
109
|
+
state={state}
|
|
110
|
+
isFocused={false}
|
|
111
|
+
required={required}
|
|
112
|
+
isEmptyValue
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// User should see the label with appropriate state styling
|
|
117
|
+
const label = getByTestId('input-label');
|
|
118
|
+
expect(label).toBeTruthy();
|
|
119
|
+
const { children } = label.props;
|
|
120
|
+
expect(
|
|
121
|
+
Array.isArray(children) ? children.filter(Boolean).join('') : children
|
|
122
|
+
).toBe(expectedText);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('when used with accessibility features', () => {
|
|
128
|
+
it('should provide proper accessibility labeling', () => {
|
|
129
|
+
const accessibilityId = 'email-input-label';
|
|
130
|
+
const { getByTestId } = renderWithTheme(
|
|
131
|
+
<FloatingLabel
|
|
132
|
+
label="Email Address"
|
|
133
|
+
variant="text"
|
|
134
|
+
state="default"
|
|
135
|
+
isFocused={false}
|
|
136
|
+
required
|
|
137
|
+
accessibilityLabelledBy={accessibilityId}
|
|
138
|
+
isEmptyValue
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// User should have proper accessibility support
|
|
143
|
+
const label = getByTestId('input-label');
|
|
144
|
+
expect(label).toHaveProp('nativeID', accessibilityId);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('label color styling based on state', () => {
|
|
149
|
+
it.each`
|
|
150
|
+
state | isFocused | expectedColor | description
|
|
151
|
+
${'default'} | ${false} | ${'#808f91'} | ${'inactive/empty field'}
|
|
152
|
+
${'default'} | ${true} | ${'#001f23'} | ${'user is typing'}
|
|
153
|
+
${'filled'} | ${false} | ${'#001f23'} | ${'field has content'}
|
|
154
|
+
${'filled'} | ${true} | ${'#001f23'} | ${'field has content and focused'}
|
|
155
|
+
${'error'} | ${false} | ${'#cb300a'} | ${'validation error'}
|
|
156
|
+
${'error'} | ${true} | ${'#001f23'} | ${'validation error but focused'}
|
|
157
|
+
${'disabled'} | ${false} | ${'#bfc1c5'} | ${'non-interactive field'}
|
|
158
|
+
${'readonly'} | ${false} | ${'#808f91'} | ${'read-only field'}
|
|
159
|
+
`(
|
|
160
|
+
'should apply $expectedColor color for $state state when focused=$isFocused ($description)',
|
|
161
|
+
({ state, isFocused, expectedColor: _expectedColor }) => {
|
|
162
|
+
const { getByTestId } = renderWithTheme(
|
|
163
|
+
<FloatingLabel
|
|
164
|
+
label={`${state} Label`}
|
|
165
|
+
variant="text"
|
|
166
|
+
state={state}
|
|
167
|
+
isFocused={isFocused}
|
|
168
|
+
required
|
|
169
|
+
isEmptyValue={state !== 'filled'}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const label = getByTestId('input-label');
|
|
174
|
+
expect(label).toHaveProp('themeState', state);
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
describe('component behavior', () => {
|
|
179
|
+
it('should render correctly with all props', () => {
|
|
180
|
+
const { getByTestId } = renderWithTheme(
|
|
181
|
+
<FloatingLabel
|
|
182
|
+
label="Full Name"
|
|
183
|
+
variant="text"
|
|
184
|
+
state="filled"
|
|
185
|
+
isFocused
|
|
186
|
+
required={false}
|
|
187
|
+
accessibilityLabelledBy="fullname-label"
|
|
188
|
+
isEmptyValue={false}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// User should see a fully rendered floating label
|
|
193
|
+
const label = getByTestId('input-label');
|
|
194
|
+
expect(label).toBeTruthy();
|
|
195
|
+
expect(label).toHaveProp('nativeID', 'fullname-label');
|
|
196
|
+
const { children } = label.props;
|
|
197
|
+
expect(Array.isArray(children) ? children.join('') : children).toBe(
|
|
198
|
+
'Full Name (Optional)'
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TextInput as RNTextInput } from 'react-native';
|
|
3
|
+
import renderWithTheme from '../../../../testUtils/renderWithTheme';
|
|
4
|
+
import InputComponent from '../InputComponent';
|
|
5
|
+
|
|
6
|
+
describe('InputComponent', () => {
|
|
7
|
+
it('renders correctly with renderInputValue', () => {
|
|
8
|
+
const ref = React.createRef<RNTextInput>();
|
|
9
|
+
const wrapper = renderWithTheme(
|
|
10
|
+
<InputComponent
|
|
11
|
+
variant="textarea"
|
|
12
|
+
nativeInputProps={{}}
|
|
13
|
+
renderInputValue={(props) => (
|
|
14
|
+
<RNTextInput
|
|
15
|
+
{...props}
|
|
16
|
+
value="customised text"
|
|
17
|
+
testID="custom-text-input"
|
|
18
|
+
/>
|
|
19
|
+
)}
|
|
20
|
+
ref={ref}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
expect(wrapper.queryAllByTestId('custom-text-input')).toHaveLength(1);
|
|
24
|
+
expect(wrapper.queryAllByDisplayValue('customised text')).toHaveLength(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders correctly without renderInputValue', () => {
|
|
28
|
+
const ref = React.createRef<RNTextInput>();
|
|
29
|
+
const wrapper = renderWithTheme(
|
|
30
|
+
<InputComponent
|
|
31
|
+
variant="textarea"
|
|
32
|
+
nativeInputProps={{ testID: 'text-input', value: 'text input value' }}
|
|
33
|
+
ref={ref}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
expect(wrapper.queryAllByTestId('text-input')).toHaveLength(1);
|
|
37
|
+
expect(wrapper.queryAllByDisplayValue('text input value')).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, TextInput as RNTextInput } from 'react-native';
|
|
3
|
+
import type { TextInputProps as NativeTextInputProps } from 'react-native';
|
|
4
|
+
// import { TextInput as RNTextInput } from 'react-native';
|
|
5
|
+
import renderWithTheme from '../../../../testUtils/renderWithTheme';
|
|
6
|
+
import InputRow from '../InputRow';
|
|
7
|
+
|
|
8
|
+
// Mock the LABEL_ANIMATION_DURATION constant
|
|
9
|
+
jest.mock('../constants', () => ({
|
|
10
|
+
LABEL_ANIMATION_DURATION: 0, // Instant animation for testing
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('InputRow', () => {
|
|
14
|
+
const defaultProps = {
|
|
15
|
+
state: 'default' as const,
|
|
16
|
+
isFocused: false,
|
|
17
|
+
variant: 'text' as const,
|
|
18
|
+
nativeInputProps: {
|
|
19
|
+
value: '',
|
|
20
|
+
placeholder: 'Enter text',
|
|
21
|
+
},
|
|
22
|
+
isEmptyValue: true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('when user sees an empty input field', () => {
|
|
30
|
+
it('should hide both prefix and input components with opacity', () => {
|
|
31
|
+
const ref = React.createRef<RNTextInput>();
|
|
32
|
+
const { getByTestId } = renderWithTheme(
|
|
33
|
+
<InputRow {...defaultProps} prefix="search" ref={ref} />
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Components should be present but hidden
|
|
37
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
38
|
+
expect(inputWrapper).toBeTruthy();
|
|
39
|
+
expect(inputWrapper).toHaveProp('accessibilityElementsHidden', true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('when user focuses the input', () => {
|
|
44
|
+
it('should show both prefix and input components with animation', () => {
|
|
45
|
+
const ref = React.createRef<RNTextInput>();
|
|
46
|
+
const { getByTestId } = renderWithTheme(
|
|
47
|
+
<InputRow {...defaultProps} isFocused isEmptyValue ref={ref} />
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Should be visible when focused
|
|
51
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
52
|
+
expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('when user enters text in the input', () => {
|
|
57
|
+
it('should keep components visible even when not focused', () => {
|
|
58
|
+
const ref = React.createRef<RNTextInput>();
|
|
59
|
+
const { getByTestId } = renderWithTheme(
|
|
60
|
+
<InputRow
|
|
61
|
+
{...defaultProps}
|
|
62
|
+
prefix="search"
|
|
63
|
+
state="filled"
|
|
64
|
+
isEmptyValue={false}
|
|
65
|
+
nativeInputProps={{
|
|
66
|
+
...defaultProps.nativeInputProps,
|
|
67
|
+
value: 'user@example.com',
|
|
68
|
+
}}
|
|
69
|
+
ref={ref}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// User has entered text - components should be visible
|
|
74
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
75
|
+
expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('when user uses input with prefix icon', () => {
|
|
80
|
+
it('should render input wrapper when prefix is provided', () => {
|
|
81
|
+
const ref = React.createRef<RNTextInput>();
|
|
82
|
+
const { getByTestId } = renderWithTheme(
|
|
83
|
+
<InputRow
|
|
84
|
+
{...defaultProps}
|
|
85
|
+
prefix="search"
|
|
86
|
+
isFocused
|
|
87
|
+
isEmptyValue={false}
|
|
88
|
+
ref={ref}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// User should see the input wrapper with prefix
|
|
93
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
94
|
+
expect(inputWrapper).toBeTruthy();
|
|
95
|
+
expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('when user uses input with custom prefix', () => {
|
|
100
|
+
it('should render custom prefix element', () => {
|
|
101
|
+
const CustomPrefix = () => <Text testID="custom-prefix">Custom</Text>;
|
|
102
|
+
const ref = React.createRef<RNTextInput>();
|
|
103
|
+
|
|
104
|
+
const { getByTestId } = renderWithTheme(
|
|
105
|
+
<InputRow
|
|
106
|
+
{...defaultProps}
|
|
107
|
+
prefix={<CustomPrefix />}
|
|
108
|
+
isFocused
|
|
109
|
+
isEmptyValue={false}
|
|
110
|
+
ref={ref}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// User should see the custom prefix
|
|
115
|
+
expect(getByTestId('custom-prefix')).toBeTruthy();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('when user uses textarea variant', () => {
|
|
120
|
+
it('should render input component with textarea variant', () => {
|
|
121
|
+
const ref = React.createRef<RNTextInput>();
|
|
122
|
+
const { getByTestId } = renderWithTheme(
|
|
123
|
+
<InputRow
|
|
124
|
+
{...defaultProps}
|
|
125
|
+
variant="textarea"
|
|
126
|
+
isFocused
|
|
127
|
+
isEmptyValue={false}
|
|
128
|
+
nativeInputProps={{
|
|
129
|
+
...defaultProps.nativeInputProps,
|
|
130
|
+
multiline: true,
|
|
131
|
+
numberOfLines: 4,
|
|
132
|
+
}}
|
|
133
|
+
ref={ref}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// User should see the textarea input
|
|
138
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
139
|
+
expect(inputWrapper).toBeTruthy();
|
|
140
|
+
expect(inputWrapper).toHaveProp('accessibilityLabel', 'Text input field');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('when user encounters different input states', () => {
|
|
145
|
+
it('should render input wrapper in error state', () => {
|
|
146
|
+
const ref = React.createRef<RNTextInput>();
|
|
147
|
+
const { getByTestId } = renderWithTheme(
|
|
148
|
+
<InputRow
|
|
149
|
+
{...defaultProps}
|
|
150
|
+
state="error"
|
|
151
|
+
isEmptyValue={false}
|
|
152
|
+
ref={ref}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Components should render with error state
|
|
157
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
158
|
+
expect(inputWrapper).toBeTruthy();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should handle disabled state appropriately', () => {
|
|
162
|
+
const ref = React.createRef<RNTextInput>();
|
|
163
|
+
const { getByTestId } = renderWithTheme(
|
|
164
|
+
<InputRow {...defaultProps} state="disabled" isEmptyValue ref={ref} />
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Even disabled inputs should render components
|
|
168
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
169
|
+
expect(inputWrapper).toBeTruthy();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('when user uses custom input renderer', () => {
|
|
174
|
+
it('should use custom render function for input value', () => {
|
|
175
|
+
const customRenderer = (inputProps: NativeTextInputProps) => (
|
|
176
|
+
<Text testID="custom-input-renderer">Custom: {inputProps.value}</Text>
|
|
177
|
+
);
|
|
178
|
+
const ref = React.createRef<RNTextInput>();
|
|
179
|
+
|
|
180
|
+
const { getByTestId } = renderWithTheme(
|
|
181
|
+
<InputRow
|
|
182
|
+
{...defaultProps}
|
|
183
|
+
renderInputValue={customRenderer}
|
|
184
|
+
isFocused
|
|
185
|
+
isEmptyValue={false}
|
|
186
|
+
nativeInputProps={{
|
|
187
|
+
...defaultProps.nativeInputProps,
|
|
188
|
+
value: 'test value',
|
|
189
|
+
}}
|
|
190
|
+
ref={ref}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// User should see the custom rendered input
|
|
195
|
+
expect(getByTestId('custom-input-renderer')).toBeTruthy();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('input reference handling', () => {
|
|
200
|
+
it('should properly handle input ref', () => {
|
|
201
|
+
const ref = React.createRef<RNTextInput>();
|
|
202
|
+
renderWithTheme(
|
|
203
|
+
<InputRow {...defaultProps} isFocused isEmptyValue={false} ref={ref} />
|
|
204
|
+
);
|
|
205
|
+
expect(ref.current).toBeDefined();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('accessibility features', () => {
|
|
210
|
+
it('should provide proper accessibility labels for input wrapper', () => {
|
|
211
|
+
const ref = React.createRef<RNTextInput>();
|
|
212
|
+
const { getByTestId } = renderWithTheme(
|
|
213
|
+
<InputRow {...defaultProps} isFocused isEmptyValue={false} ref={ref} />
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// User should have proper accessibility support
|
|
217
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
218
|
+
expect(inputWrapper).toHaveProp('accessibilityLabel', 'Text input field');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should hide components from screen readers when not visible', () => {
|
|
222
|
+
const ref = React.createRef<RNTextInput>();
|
|
223
|
+
const { getByTestId } = renderWithTheme(
|
|
224
|
+
<InputRow
|
|
225
|
+
{...defaultProps}
|
|
226
|
+
prefix="search"
|
|
227
|
+
state="default"
|
|
228
|
+
isEmptyValue
|
|
229
|
+
ref={ref}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Components should be hidden from screen readers when not visible
|
|
234
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
235
|
+
expect(inputWrapper).toHaveProp('accessibilityElementsHidden', true);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('component behavior', () => {
|
|
240
|
+
it('should render correctly with all props', () => {
|
|
241
|
+
const ref = React.createRef<RNTextInput>();
|
|
242
|
+
const { getByTestId } = renderWithTheme(
|
|
243
|
+
<InputRow
|
|
244
|
+
{...defaultProps}
|
|
245
|
+
prefix="search"
|
|
246
|
+
isFocused
|
|
247
|
+
isEmptyValue={false}
|
|
248
|
+
nativeInputProps={{
|
|
249
|
+
...defaultProps.nativeInputProps,
|
|
250
|
+
value: 'test@example.com',
|
|
251
|
+
placeholder: 'Enter email',
|
|
252
|
+
}}
|
|
253
|
+
ref={ref}
|
|
254
|
+
/>
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// User should see a fully rendered input row
|
|
258
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
259
|
+
expect(inputWrapper).toBeTruthy();
|
|
260
|
+
expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should render without prefix', () => {
|
|
264
|
+
const ref = React.createRef<RNTextInput>();
|
|
265
|
+
const { getByTestId, queryByText } = renderWithTheme(
|
|
266
|
+
<InputRow {...defaultProps} ref={ref} />
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// User should see input without prefix
|
|
270
|
+
const inputWrapper = getByTestId('input-row-input-wrapper');
|
|
271
|
+
expect(inputWrapper).toBeTruthy();
|
|
272
|
+
expect(queryByText('search')).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderWithTheme from '../../../../testUtils/renderWithTheme';
|
|
3
|
+
import MaxLengthMessage from '../MaxLengthMessage';
|
|
4
|
+
|
|
5
|
+
describe('MaxLengthMessage', () => {
|
|
6
|
+
it('renders correctly with maxLength', () => {
|
|
7
|
+
const { queryAllByText } = renderWithTheme(
|
|
8
|
+
<MaxLengthMessage
|
|
9
|
+
maxLength={10}
|
|
10
|
+
state="default"
|
|
11
|
+
currentLength={5}
|
|
12
|
+
hideCharacterCount={false}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
expect(queryAllByText('5/10')).toHaveLength(1);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderWithTheme from '../../../../testUtils/renderWithTheme';
|
|
3
|
+
import PrefixComponent from '../PrefixComponent';
|
|
4
|
+
|
|
5
|
+
describe('PrefixComponent', () => {
|
|
6
|
+
it('renders prefix icon', () => {
|
|
7
|
+
const wrapper = renderWithTheme(
|
|
8
|
+
<PrefixComponent prefix="dollar-sign" state="default" />
|
|
9
|
+
);
|
|
10
|
+
expect(wrapper.getByA11yLabel('Prefix icon: dollar-sign')).toBeDefined();
|
|
11
|
+
// Optionally, keep testID fallback
|
|
12
|
+
expect(wrapper.getByTestId('input-prefix')).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderWithTheme from '../../../../testUtils/renderWithTheme';
|
|
3
|
+
import {
|
|
4
|
+
StyledHelperText,
|
|
5
|
+
StyledTextInput,
|
|
6
|
+
StyledCharacterCount,
|
|
7
|
+
StyledError,
|
|
8
|
+
StyledErrorRow,
|
|
9
|
+
StyledLabel,
|
|
10
|
+
StyledBorder,
|
|
11
|
+
} from '../StyledTextInput';
|
|
12
|
+
|
|
13
|
+
describe('StyledLabel', () => {
|
|
14
|
+
it.each`
|
|
15
|
+
themeState
|
|
16
|
+
${'default'}
|
|
17
|
+
${'filled'}
|
|
18
|
+
${'error'}
|
|
19
|
+
${'disabled'}
|
|
20
|
+
${'readonly'}
|
|
21
|
+
`('renders correctly with themeState $themeState', ({ themeState }): void => {
|
|
22
|
+
const { toJSON } = renderWithTheme(
|
|
23
|
+
<StyledLabel themeState={themeState}>Label</StyledLabel>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(toJSON()).toMatchSnapshot();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('StyledErrorRow', () => {
|
|
31
|
+
it('renders correctly', (): void => {
|
|
32
|
+
const { toJSON } = renderWithTheme(<StyledErrorRow />);
|
|
33
|
+
|
|
34
|
+
expect(toJSON()).toMatchSnapshot();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('StyledError', () => {
|
|
39
|
+
it('renders correctly', (): void => {
|
|
40
|
+
const { toJSON } = renderWithTheme(
|
|
41
|
+
<StyledError>must not exceed character limit</StyledError>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(toJSON()).toMatchSnapshot();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('StyledCharacterCount', () => {
|
|
49
|
+
it.each`
|
|
50
|
+
themeState
|
|
51
|
+
${'default'}
|
|
52
|
+
${'filled'}
|
|
53
|
+
${'error'}
|
|
54
|
+
${'disabled'}
|
|
55
|
+
${'readonly'}
|
|
56
|
+
`('renders correctly with themeState $themeState', ({ themeState }): void => {
|
|
57
|
+
const { toJSON } = renderWithTheme(
|
|
58
|
+
<StyledCharacterCount themeState={themeState}>
|
|
59
|
+
100/255
|
|
60
|
+
</StyledCharacterCount>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(toJSON()).toMatchSnapshot();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('StyledHelperText', () => {
|
|
68
|
+
it('renders correctly', (): void => {
|
|
69
|
+
const { toJSON } = renderWithTheme(
|
|
70
|
+
<StyledHelperText>helper text</StyledHelperText>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(toJSON()).toMatchSnapshot();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('StyledBorder', () => {
|
|
78
|
+
it.each`
|
|
79
|
+
themeState
|
|
80
|
+
${'default'}
|
|
81
|
+
${'filled'}
|
|
82
|
+
${'error'}
|
|
83
|
+
${'disabled'}
|
|
84
|
+
${'readonly'}
|
|
85
|
+
`('renders correctly with themeState $themeState', ({ themeState }): void => {
|
|
86
|
+
const { toJSON } = renderWithTheme(
|
|
87
|
+
<StyledBorder themeState={themeState} themeFocused={false} />
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(toJSON()).toMatchSnapshot();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('renders correctly when focused', (): void => {
|
|
94
|
+
const { toJSON } = renderWithTheme(
|
|
95
|
+
<StyledBorder themeState="error" themeFocused />
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(toJSON()).toMatchSnapshot();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('StyledTextInput', () => {
|
|
103
|
+
it.each`
|
|
104
|
+
themeVariant
|
|
105
|
+
${'text'}
|
|
106
|
+
${'textarea'}
|
|
107
|
+
`('renders correctly with $themeState state', ({ themeVariant }) => {
|
|
108
|
+
const { toJSON } = renderWithTheme(
|
|
109
|
+
<StyledTextInput themeVariant={themeVariant} />
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(toJSON()).toMatchSnapshot();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderWithTheme from '../../../../testUtils/renderWithTheme';
|
|
3
|
+
import SuffixComponent from '../SuffixComponent';
|
|
4
|
+
|
|
5
|
+
describe('SuffixComponent', () => {
|
|
6
|
+
it('renders loading icon with loading', () => {
|
|
7
|
+
const wrapper = renderWithTheme(
|
|
8
|
+
<SuffixComponent loading suffix="dollar-sign" state="default" />
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
expect(wrapper.getByA11yLabel('Suffix icon: loading')).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('renders suffix icon', () => {
|
|
15
|
+
const wrapper = renderWithTheme(
|
|
16
|
+
<SuffixComponent loading={false} suffix="dollar-sign" state="default" />
|
|
17
|
+
);
|
|
18
|
+
expect(wrapper.getByA11yLabel('Suffix icon: dollar-sign')).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
});
|