input-otp-native
Version:
One time passcode Input For React Native/Expo. Unstyled and fully customizable.
310 lines (299 loc) • 12.8 kB
JavaScript
"use strict";
var _reactNative = require("react-native");
var React = _interopRequireWildcard(require("react"));
var _reactNative2 = require("@testing-library/react-native");
var _input = require("./input.js");
var _jsxRuntime = require("react/jsx-runtime");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
afterEach(_reactNative2.cleanup);
afterEach(() => {
// Reset Platform.OS mock after each test
_reactNative.Platform.OS = 'ios';
});
const onChangeMock = jest.fn();
const onCompleteMock = jest.fn();
const defaultRender = props => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
testID: "otp-cells",
"data-focused": props.isFocused,
children: props.slots.map((slot, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
testID: `otp-cell-${index}`,
style: {
opacity: props.isFocused ? 1 : 0.5
},
children: slot.char && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
children: slot.char
})
}, index))
});
/**
* Simulate typing on the input like a user would do by typing one character at a time.
*
* @param input - The input element to simulate typing on.
* @param text - The text to simulate typing.
*/
const simulateTyping = (input, text) => {
let accumulated = '';
for (const char of text) {
accumulated += char;
_reactNative2.fireEvent.changeText(input, accumulated);
(0, _reactNative2.fireEvent)(input, 'keyPress', {
nativeEvent: {
key: char
}
});
}
};
// 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 () => {
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
expect(input).toBeTruthy();
expect(input.props.inputMode).toBe('numeric');
expect(input.props.autoComplete).toBe('one-time-code');
});
test('sets correct autoComplete value for Android', async () => {
_reactNative.Platform.OS = 'android';
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
expect(input.props.autoComplete).toBe('sms-otp');
});
test('sets correct autoComplete value for iOS', async () => {
_reactNative.Platform.OS = 'ios';
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
expect(input.props.autoComplete).toBe('one-time-code');
});
test('renders correctly without render prop', async () => {
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 6
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
expect(input).toBeTruthy();
// The container should still be rendered
const container = await _reactNative2.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 = '******';
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 4,
placeholder: placeholder,
inputMode: "text",
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
expect(input.props.placeholder).toBe(placeholder);
expect(input.props.inputMode).toBe('text');
});
test('renders custom content using render prop', async () => {
const customRender = props => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
testID: "custom-content",
children: props.slots.map((slot, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
children: slot.char || slot.placeholderChar
}, index))
});
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 4,
render: customRender
}));
expect(await _reactNative2.screen.findByTestId('custom-content')).toBeTruthy();
});
});
describe('Interactions', () => {
test('handles text input correctly', async () => {
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 6,
onComplete: onCompleteMock,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
simulateTyping(input, '123456');
expect(onChangeMock).toHaveBeenCalledWith('123456');
expect(onCompleteMock).toHaveBeenCalledWith('123456');
});
test('onComplete is called when maxLength is reached only', async () => {
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 6,
onComplete: onCompleteMock,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
simulateTyping(input, '12345');
expect(onChangeMock).toHaveBeenCalledWith('12345');
expect(onCompleteMock).not.toHaveBeenCalled();
simulateTyping(input, '123456');
expect(onChangeMock).toHaveBeenCalledWith('123456');
expect(onCompleteMock).toHaveBeenCalledWith('123456');
});
test('clears input on container press', async () => {
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
simulateTyping(input, '123');
const container = await _reactNative2.screen.findByTestId('otp-input-container');
_reactNative2.fireEvent.press(container);
expect(input.props.value).toBe('');
});
test('handles focus and blur events', async () => {
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
const cells = await _reactNative2.screen.findByTestId('otp-cells');
(0, _reactNative2.fireEvent)(input, 'focus');
expect(cells.props['data-focused']).toBe(true);
(0, _reactNative2.fireEvent)(input, 'blur');
expect(cells.props['data-focused']).toBe(false);
});
});
describe('Validation', () => {
test('respects pattern prop for input validation', async () => {
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 6,
pattern: "^[0-9]+$",
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
// Test invalid input
simulateTyping(input, 'abc');
expect(onChangeMock).toHaveBeenCalledTimes(2);
expect(onChangeMock).toHaveBeenCalledWith('');
expect(input.props.value).toBe('');
// Test valid input
simulateTyping(input, '123');
expect(onChangeMock).toHaveBeenCalledWith('123');
expect(input.props.value).toBe('123');
});
test('respects maxLength prop', async () => {
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
onChange: onChangeMock,
maxLength: 4,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
simulateTyping(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();
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
ref: ref,
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
await (0, _reactNative2.act)(async () => {
ref.current?.setValue('1');
});
expect(input.props.value).toBe('1');
expect(onChangeMock).toHaveBeenCalledWith('1');
});
test('focus method focuses the input through ref', async () => {
const ref = /*#__PURE__*/React.createRef();
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
ref: ref,
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const cells = await _reactNative2.screen.findByTestId('otp-cells');
const input = await _reactNative2.screen.findByTestId('otp-input');
await (0, _reactNative2.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
(0, _reactNative2.fireEvent)(input, 'focus');
});
expect(cells.props['data-focused']).toBe(true);
});
test('blur method blurs the input through ref', async () => {
const ref = /*#__PURE__*/React.createRef();
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
ref: ref,
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
const cells = await _reactNative2.screen.findByTestId('otp-cells');
// First focus
await (0, _reactNative2.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
(0, _reactNative2.fireEvent)(input, 'focus');
});
expect(cells.props['data-focused']).toBe(true);
// Then blur
await (0, _reactNative2.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
(0, _reactNative2.fireEvent)(input, 'blur');
});
expect(cells.props['data-focused']).toBe(false);
});
test('clear method clears the input through ref', async () => {
const ref = /*#__PURE__*/React.createRef();
(0, _reactNative2.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_input.OTPInput, {
ref: ref,
onChange: onChangeMock,
maxLength: 6,
render: defaultRender
}));
const input = await _reactNative2.screen.findByTestId('otp-input');
// First set a value
await (0, _reactNative2.act)(async () => {
ref.current?.setValue('1');
});
expect(input.props.value).toBe('1');
// Then clear it
await (0, _reactNative2.act)(async () => {
ref.current?.clear();
});
expect(input.props.value).toBe('');
});
});
});
//# sourceMappingURL=input.test.js.map