Skip to content

Commit e7f0de1

Browse files
authored
Merge pull request #2 from johelder/feature/unit-tests
feature/unit-tests
2 parents ffddb10 + 317e616 commit e7f0de1

File tree

8 files changed

+416
-20
lines changed

8 files changed

+416
-20
lines changed

eslint.config.mjs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ const compat = new FlatCompat({
1616

1717
export default defineConfig([
1818
{
19-
extends: fixupConfigRules(compat.extends('@react-native', 'prettier')),
19+
files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
20+
extends: fixupConfigRules(
21+
compat.extends(
22+
'@react-native',
23+
'prettier',
24+
'plugin:testing-library/react'
25+
)
26+
),
2027
plugins: { prettier },
2128
rules: {
2229
'react/react-in-jsx-scope': 'off',
@@ -30,12 +37,10 @@ export default defineConfig([
3037
useTabs: false,
3138
},
3239
],
40+
'testing-library/prefer-screen-queries': 'off',
3341
},
3442
},
3543
{
36-
ignores: [
37-
'node_modules/',
38-
'lib/'
39-
],
44+
ignores: ['node_modules/', 'lib/'],
4045
},
4146
]);

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,21 @@
8888
"@evilmartians/lefthook": "^1.5.0",
8989
"@react-native/eslint-config": "^0.78.0",
9090
"@release-it/conventional-changelog": "^9.0.2",
91+
"@testing-library/react-native": "^13.2.0",
9192
"@types/jest": "^29.5.5",
9293
"@types/react": "^19.0.12",
9394
"commitlint": "^19.6.1",
9495
"del-cli": "^5.1.0",
9596
"eslint": "^9.22.0",
9697
"eslint-config-prettier": "^10.1.1",
9798
"eslint-plugin-prettier": "^5.2.3",
99+
"eslint-plugin-testing-library": "^7.2.2",
98100
"jest": "^29.7.0",
99101
"prettier": "^3.0.3",
100102
"react": "18.3.1",
101103
"react-native": "0.76.8",
102-
"react-native-builder-bob": "^0.39.0",
104+
"react-native-builder-bob": "^0.40.11",
105+
"react-test-renderer": "18.3.1",
103106
"release-it": "^17.10.0",
104107
"typescript": "^5.2.2"
105108
},
@@ -176,6 +179,6 @@
176179
"create-react-native-library": {
177180
"languages": "js",
178181
"type": "library",
179-
"version": "0.49.0"
182+
"version": "0.50.3"
180183
}
181184
}

src/__tests__/text-input-otp.test.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { act, fireEvent, render } from '@testing-library/react-native';
2+
import { TextInputOTP, TextInputOTPSlot } from '../components';
3+
import type { TextInputOTPProps, TextInputOTPRef } from '../types';
4+
import { createRef } from 'react';
5+
6+
function renderTextInputOTP({
7+
maxLength = 6,
8+
...rest
9+
}: Partial<TextInputOTPProps>) {
10+
return render(
11+
<TextInputOTP maxLength={maxLength} {...rest}>
12+
<TextInputOTPSlot index={0} />
13+
<TextInputOTPSlot index={1} />
14+
<TextInputOTPSlot index={2} />
15+
<TextInputOTPSlot index={3} />
16+
<TextInputOTPSlot index={4} />
17+
<TextInputOTPSlot index={5} />
18+
</TextInputOTP>
19+
);
20+
}
21+
22+
describe('TextInputOTP Component', () => {
23+
beforeAll(() => {
24+
jest.resetAllMocks();
25+
});
26+
27+
it('should call onFilled with the complete code when all digits are filled', () => {
28+
const CODE = '123456';
29+
const mockedOnFilled = jest.fn();
30+
const view = renderTextInputOTP({ onFilled: mockedOnFilled });
31+
fireEvent.changeText(view.getByTestId('hidden-text-input'), CODE);
32+
expect(mockedOnFilled).toHaveBeenCalledWith(CODE);
33+
});
34+
35+
it('should not render the animated caret when the caretHidden prop is true', () => {
36+
const view = renderTextInputOTP({ caretHidden: true });
37+
expect(view.queryByTestId('caret')).toBeNull();
38+
});
39+
40+
it('should render the slots only up to the number defined by the maxLength prop', () => {
41+
const MAX_LENGTH = 6;
42+
const view = renderTextInputOTP({
43+
maxLength: MAX_LENGTH,
44+
caretHidden: true,
45+
});
46+
const slots = view.getAllByTestId('text-input-otp-slot');
47+
expect(slots).toHaveLength(MAX_LENGTH);
48+
});
49+
50+
it('should call onFilled with the complete code when setValue is called programmatically', () => {
51+
const CODE = '123';
52+
const mockedOnFilled = jest.fn();
53+
const ref = createRef<TextInputOTPRef>();
54+
render(
55+
<TextInputOTP ref={ref} maxLength={3} onFilled={mockedOnFilled}>
56+
<TextInputOTPSlot index={0} />
57+
<TextInputOTPSlot index={1} />
58+
<TextInputOTPSlot index={2} />
59+
</TextInputOTP>
60+
);
61+
act(() => ref.current?.setValue(CODE));
62+
expect(mockedOnFilled).toHaveBeenCalledWith(CODE);
63+
});
64+
65+
it('should call clear the input text when clear function is called programmatically', () => {
66+
const CODE = '1';
67+
const ref = createRef<TextInputOTPRef>();
68+
const view = render(
69+
<TextInputOTP ref={ref} maxLength={3}>
70+
<TextInputOTPSlot index={0} />
71+
<TextInputOTPSlot index={1} />
72+
<TextInputOTPSlot index={2} />
73+
</TextInputOTP>
74+
);
75+
fireEvent.changeText(view.getByTestId('hidden-text-input'), CODE);
76+
act(() => ref.current?.clear());
77+
expect(view.queryByText(CODE)).toBeFalsy();
78+
});
79+
});

src/components/caret.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function Caret() {
2626

2727
return (
2828
<Animated.View
29+
testID="caret"
2930
style={[
3031
styles.caret,
3132
{ opacity, backgroundColor: caretColor ?? DEFAULT_DARK_COLOR },

src/components/text-input-otp-slot.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Pressable, Text, StyleSheet } from 'react-native';
33
import { Caret } from './caret';
44
import { useTextInputOTP } from '../hooks/use-text-input-otp';
55
import { useSlotBorderStyles } from '../hooks/use-slot-border-styles';
6-
import { SLOT_HEIGHT, SLOT_WIDTH } from '../constants';
6+
import { DEFAULT_DARK_COLOR, SLOT_HEIGHT, SLOT_WIDTH } from '../constants';
77
import type {
88
TextInputOTPSlotInternalProps,
99
TextInputOTPSlotProps,
@@ -26,6 +26,7 @@ function TextInputOTPSlotComponent({
2626

2727
return (
2828
<Pressable
29+
testID="text-input-otp-slot"
2930
onPress={handlePress}
3031
style={StyleSheet.flatten([
3132
styles.slot,
@@ -60,7 +61,7 @@ const styles = StyleSheet.create({
6061
alignItems: 'center',
6162
},
6263
slotText: {
63-
color: '#030712',
64+
color: DEFAULT_DARK_COLOR,
6465
fontSize: 14,
6566
fontWeight: 'bold',
6667
},

src/components/text-input.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const TextInput = forwardRef<
1919

2020
return (
2121
<RNTextInput
22+
testID="hidden-text-input"
2223
ref={inputRef}
2324
keyboardType="number-pad"
2425
autoFocus={autoFocus}

src/hooks/use-text-input-otp.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export function TextInputOTPProvider({
7070
text.length > maxLength ? text.slice(0, maxLength) : text;
7171
setCode(filledCode);
7272
setCurrentIndex(maxLength - 1);
73+
onFilled?.(filledCode);
7374
},
7475
[maxLength]
7576
);

0 commit comments

Comments
 (0)