input-otp-native
Version:
One time passcode Input For React Native/Expo. Unstyled and fully customizable.
290 lines (280 loc) • 10.3 kB
JavaScript
"use strict";
import { View, Text, Platform } from 'react-native';
import * as React from 'react';
import { cleanup, render, screen, fireEvent, act } from '@testing-library/react-native';
import { OTPInput } from "./input.js";
import { jsx as _jsx } from "react/jsx-runtime";
afterEach(cleanup);
afterEach(() => {
// Reset Platform.OS mock after each test
Platform.OS = 'ios';
});
const onChangeMock = jest.fn();
const onCompleteMock = jest.fn();
const defaultRender = props => /*#__PURE__*/_jsx(View, {
testID: "otp-cells",
"data-focused": props.isFocused,
children: props.slots.map((slot, index) => /*#__PURE__*/_jsx(View, {
testID: `otp-cell-${index}`,
style: {
opacity: props.isFocused ? 1 : 0.5
},
children: slot.char && /*#__PURE__*/_jsx(Text, {
children: slot.char
})
}, index))
});
// Mock Platform.OS
jest.mock('react-native/Libraries/Utilities/Platform', () => ({
OS: 'ios',
select: jest.fn()
}));
describe('OTPInput', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Rendering', () => {
test('renders correctly with default props', async () => {
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
expect(input).toBeTruthy();
expect(input.props.maxLength).toBe(6);
expect(input.props.inputMode).toBe('numeric');
expect(input.props.autoComplete).toBe('one-time-code');
});
test('sets correct autoComplete value for Android', async () => {
Platform.OS = 'android';
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
expect(input.props.autoComplete).toBe('sms-otp');
});
test('sets correct autoComplete value for iOS', async () => {
Platform.OS = 'ios';
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
expect(input.props.autoComplete).toBe('one-time-code');
});
test('renders correctly without render prop', async () => {
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 6
}));
const input = await screen.findByTestId('otp-input');
expect(input).toBeTruthy();
// The container should still be rendered
const container = await screen.findByTestId('otp-input-container');
expect(container).toBeTruthy();
// Container should have exactly two children:
// 1. null from the render prop (React still counts this)
// 2. The TextInput component
const customContent = container.children.length;
expect(customContent).toBe(2);
});
test('renders with custom props', async () => {
const placeholder = '******';
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 4,
placeholder: placeholder,
inputMode: "text",
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
expect(input.props.maxLength).toBe(4);
expect(input.props.placeholder).toBe(placeholder);
expect(input.props.inputMode).toBe('text');
});
test('renders custom content using render prop', async () => {
const customRender = props => /*#__PURE__*/_jsx(View, {
testID: "custom-content",
children: props.slots.map((slot, index) => /*#__PURE__*/_jsx(Text, {
children: slot.char || slot.placeholderChar
}, index))
});
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 4,
render: customRender
}));
expect(await screen.findByTestId('custom-content')).toBeTruthy();
});
});
describe('Interactions', () => {
test('handles text input correctly', async () => {
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 6,
onComplete: onCompleteMock,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
fireEvent.changeText(input, '123456');
expect(onChangeMock).toHaveBeenCalledWith('123456');
expect(onCompleteMock).toHaveBeenCalledWith('123456');
});
test('onComplete is called when maxLength is reached only', async () => {
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 6,
onComplete: onCompleteMock,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
fireEvent.changeText(input, '12345');
expect(onChangeMock).toHaveBeenCalledWith('12345');
expect(onCompleteMock).not.toHaveBeenCalled();
fireEvent.changeText(input, '123456');
expect(onChangeMock).toHaveBeenCalledWith('123456');
expect(onCompleteMock).toHaveBeenCalledWith('123456');
});
test('clears input on container press', async () => {
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
fireEvent.changeText(input, '123');
const container = await screen.findByTestId('otp-input-container');
fireEvent.press(container);
expect(input.props.value).toBe('');
});
test('handles focus and blur events', async () => {
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
const cells = await screen.findByTestId('otp-cells');
fireEvent(input, 'focus');
expect(cells.props['data-focused']).toBe(true);
fireEvent(input, 'blur');
expect(cells.props['data-focused']).toBe(false);
});
});
describe('Validation', () => {
test('respects pattern prop for input validation', async () => {
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 6,
pattern: "^[0-9]+$",
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
// Test invalid input
fireEvent.changeText(input, 'abc');
expect(onChangeMock).not.toHaveBeenCalled();
expect(input.props.value).toBe('');
// Test valid input
fireEvent.changeText(input, '123');
expect(onChangeMock).toHaveBeenCalledWith('123');
expect(input.props.value).toBe('123');
});
test('respects maxLength prop', async () => {
render(/*#__PURE__*/_jsx(OTPInput, {
onChange: onChangeMock,
maxLength: 4,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
fireEvent.changeText(input, '123456');
expect(input.props.value.length).toBeLessThanOrEqual(4);
});
});
describe('Ref Methods', () => {
test('setValue updates input value through ref', async () => {
const ref = /*#__PURE__*/React.createRef();
render(/*#__PURE__*/_jsx(OTPInput, {
ref: ref,
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
await act(async () => {
ref.current?.setValue('123');
});
expect(input.props.value).toBe('123');
expect(onChangeMock).toHaveBeenCalledWith('123');
});
test('focus method focuses the input through ref', async () => {
const ref = /*#__PURE__*/React.createRef();
render(/*#__PURE__*/_jsx(OTPInput, {
ref: ref,
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const cells = await screen.findByTestId('otp-cells');
const input = await screen.findByTestId('otp-input');
await act(async () => {
ref.current?.focus();
// we need to call onFocus by fireEvent because test environment do not support onFocus
// see the issue https://github.com/callstack/react-native-testing-library/issues/1069
fireEvent(input, 'focus');
});
expect(cells.props['data-focused']).toBe(true);
});
test('blur method blurs the input through ref', async () => {
const ref = /*#__PURE__*/React.createRef();
render(/*#__PURE__*/_jsx(OTPInput, {
ref: ref,
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
const cells = await screen.findByTestId('otp-cells');
// First focus
await act(async () => {
ref.current?.focus();
// we need to call onFocus by fireEvent because test environment do not support onFocus
// see the issue https://github.com/callstack/react-native-testing-library/issues/1069
fireEvent(input, 'focus');
});
expect(cells.props['data-focused']).toBe(true);
// Then blur
await act(async () => {
ref.current?.blur();
// we need to call onBlur by fireEvent because test environment do not support onBlur
// see the issue https://github.com/callstack/react-native-testing-library/issues/1069
fireEvent(input, 'blur');
});
expect(cells.props['data-focused']).toBe(false);
});
test('clear method clears the input through ref', async () => {
const ref = /*#__PURE__*/React.createRef();
render(/*#__PURE__*/_jsx(OTPInput, {
ref: ref,
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await screen.findByTestId('otp-input');
// First set a value
await act(async () => {
ref.current?.setValue('123');
});
expect(input.props.value).toBe('123');
// Then clear it
await act(async () => {
ref.current?.clear();
});
expect(input.props.value).toBe('');
});
});
});
//# sourceMappingURL=input.test.js.map