From 25be7b3e322dcb519d956cf286ca8e8d056089b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 4 Aug 2025 02:32:14 -0700 Subject: [PATCH] Make TextInput test follow convention and add tests for all methods Summary: Changelog: [internal] This restructures the test for `TextInput` to follow the convention: ``` describe('', () => { describe('props', () => { /* ... */ }); describe('ref', () => { /* ... */ }); }); ``` It also adds tests for all methods. Differential Revision: D79511032 --- .../TextInput/__tests__/TextInput-itest.js | 491 ++++++++++++------ 1 file changed, 341 insertions(+), 150 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js index d1cdd903124a..ed770b004c49 100644 --- a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js +++ b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js @@ -10,213 +10,404 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; -import type {HostInstance} from 'react-native'; +import type {TextInputInstance} from '../TextInput.flow'; import ensureInstance from '../../../../src/private/__tests__/utilities/ensureInstance'; import * as Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; import * as React from 'react'; import {createRef, useEffect, useLayoutEffect, useRef} from 'react'; import {TextInput} from 'react-native'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; -describe('focus view command', () => { - it('creates view before dispatching view command from ref function', () => { - const root = Fantom.createRoot(); - - Fantom.runTask(() => { - root.render( - { - if (node) { - node.focus(); - } - }} - />, - ); +describe('', () => { + describe('props', () => { + describe('selection', () => { + it('the selection is passed to component view by command', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + hello World! + , + ); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + 'Create {type: "AndroidTextInput", nativeID: "text-input"}', + 'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}', + 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "setTextAndSelection, args: [0,null,0,4]"}', + ]); + }); }); - expect(root.takeMountingManagerLogs()).toEqual([ - 'Update {type: "RootView", nativeID: (root)}', - 'Create {type: "AndroidTextInput", nativeID: "text-input"}', - 'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}', - 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}', - ]); - }); + describe('onChange', () => { + it('is called when the change native event is dispatched', () => { + const root = Fantom.createRoot(); + const nodeRef = createRef(); + const onChange = jest.fn(); + + Fantom.runTask(() => { + root.render( + { + onChange(event.nativeEvent); + }} + ref={nodeRef} + />, + ); + }); + + const element = ensureInstance(nodeRef.current, ReactNativeElement); + + Fantom.runOnUIThread(() => { + Fantom.enqueueNativeEvent(element, 'change', { + text: 'Hello World', + }); + }); + + Fantom.runWorkLoop(); + + expect(onChange).toHaveBeenCalledTimes(1); + const [entry] = onChange.mock.lastCall; + expect(entry.text).toEqual('Hello World'); + }); + }); - it('creates view before dispatching view command from useLayoutEffect', () => { - const root = Fantom.createRoot(); + describe('onChangeText', () => { + it('is called when the change native event is dispatched', () => { + const root = Fantom.createRoot(); + const nodeRef = createRef(); + const onChangeText = jest.fn(); - function Component() { - const textInputRef = useRef>( - null, - ); + Fantom.runTask(() => { + root.render(); + }); - useLayoutEffect(() => { - textInputRef.current?.focus(); - }); + const element = ensureInstance(nodeRef.current, ReactNativeElement); - return ; - } - Fantom.runTask(() => { - root.render(); + Fantom.runOnUIThread(() => { + Fantom.enqueueNativeEvent(element, 'change', { + text: 'Hello World', + }); + }); + + Fantom.runWorkLoop(); + + expect(onChangeText).toHaveBeenCalledTimes(1); + const [entry] = onChangeText.mock.lastCall; + expect(entry).toEqual('Hello World'); + }); }); - expect(root.takeMountingManagerLogs()).toEqual([ - 'Update {type: "RootView", nativeID: (root)}', - 'Create {type: "AndroidTextInput", nativeID: "text-input"}', - 'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}', - 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}', - ]); - }); + describe('onFocus', () => { + it('is called when the focus native event is dispatched', () => { + const root = Fantom.createRoot(); + const nodeRef = createRef(); - it('creates view before dispatching view command from useEffect', () => { - const root = Fantom.createRoot(); + let focusEvent = jest.fn(); - function Component() { - const textInputRef = useRef>( - null, - ); + Fantom.runTask(() => { + root.render(); + }); - useEffect(() => { - textInputRef.current?.focus(); - }); + const element = ensureInstance(nodeRef.current, ReactNativeElement); + + expect(focusEvent).toHaveBeenCalledTimes(0); + + Fantom.runOnUIThread(() => { + Fantom.enqueueNativeEvent(element, 'focus'); + }); - return ; - } + // The tasks have not run. + expect(focusEvent).toHaveBeenCalledTimes(0); - Fantom.runTask(() => { - root.render(); + Fantom.runWorkLoop(); + + expect(focusEvent).toHaveBeenCalledTimes(1); + }); }); - expect(root.takeMountingManagerLogs()).toEqual([ - 'Update {type: "RootView", nativeID: (root)}', - 'Create {type: "AndroidTextInput", nativeID: "text-input"}', - 'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}', - 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}', - ]); - }); -}); + describe('onBlur', () => { + it('is called when the blur native event is dispatched', () => { + const root = Fantom.createRoot(); + const nodeRef = createRef(); -describe('focus and blur event', () => { - it('sends focus and blur events', () => { - const root = Fantom.createRoot(); - const nodeRef = createRef(); + let blurEvent = jest.fn(); - let focusEvent = jest.fn(); - let blurEvent = jest.fn(); + Fantom.runTask(() => { + root.render(); + }); - Fantom.runTask(() => { - root.render( - , - ); - }); + const element = ensureInstance(nodeRef.current, ReactNativeElement); + + expect(blurEvent).toHaveBeenCalledTimes(0); - const element = ensureInstance(nodeRef.current, ReactNativeElement); + Fantom.runOnUIThread(() => { + Fantom.enqueueNativeEvent(element, 'blur'); + }); - expect(focusEvent).toHaveBeenCalledTimes(0); - expect(blurEvent).toHaveBeenCalledTimes(0); + // The tasks have not run. + expect(blurEvent).toHaveBeenCalledTimes(0); - Fantom.runOnUIThread(() => { - Fantom.enqueueNativeEvent(element, 'focus'); + Fantom.runWorkLoop(); + + expect(blurEvent).toHaveBeenCalledTimes(1); + }); }); + }); - // The tasks have not run. - expect(focusEvent).toHaveBeenCalledTimes(0); - expect(blurEvent).toHaveBeenCalledTimes(0); + describe('ref', () => { + it('is an element node', () => { + const ref = createRef(); - Fantom.runWorkLoop(); + const root = Fantom.createRoot(); - expect(focusEvent).toHaveBeenCalledTimes(1); - expect(blurEvent).toHaveBeenCalledTimes(0); + Fantom.runTask(() => { + root.render(); + }); - Fantom.runOnUIThread(() => { - Fantom.enqueueNativeEvent(element, 'blur'); + expect(ref.current).toBeInstanceOf(ReactNativeElement); }); - Fantom.runWorkLoop(); + it('provides additional methods: clear, isFocused, getNativeRef, setSelection', () => { + const ref = createRef(); - expect(focusEvent).toHaveBeenCalledTimes(1); - expect(blurEvent).toHaveBeenCalledTimes(1); - }); -}); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); -describe('onChange', () => { - it('delivers onChange event', () => { - const root = Fantom.createRoot(); - const nodeRef = createRef(); - const onChange = jest.fn(); - - Fantom.runTask(() => { - root.render( - { - onChange(event.nativeEvent); - }} - ref={nodeRef} - />, - ); + const instance = nullthrows(ref.current); + expect(instance.clear).toBeInstanceOf(Function); + expect(instance.isFocused).toBeInstanceOf(Function); + expect(instance.getNativeRef).toBeInstanceOf(Function); }); - const element = ensureInstance(nodeRef.current, ReactNativeElement); + describe('focus()', () => { + it('dispatches the focus command', () => { + const root = Fantom.createRoot(); + const ref = createRef(); + + Fantom.runTask(() => { + root.render(); + }); + + root.takeMountingManagerLogs(); + + const instance = nullthrows(ref.current); + + Fantom.runTask(() => { + instance.focus(); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}', + ]); + }); + + it('creates view before dispatching view command from ref function', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + { + if (node) { + node.focus(); + } + }} + />, + ); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + 'Create {type: "AndroidTextInput", nativeID: "text-input"}', + 'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}', + 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}', + ]); + }); + + it('creates view before dispatching view command from useLayoutEffect', () => { + const root = Fantom.createRoot(); + + function Component() { + const textInputRef = useRef>(null); + + useLayoutEffect(() => { + textInputRef.current?.focus(); + }); + + return ; + } + Fantom.runTask(() => { + root.render(); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + 'Create {type: "AndroidTextInput", nativeID: "text-input"}', + 'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}', + 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}', + ]); + }); + + it('creates view before dispatching view command from useEffect', () => { + const root = Fantom.createRoot(); + + function Component() { + const textInputRef = useRef>(null); + + useEffect(() => { + textInputRef.current?.focus(); + }); + + return ; + } + + Fantom.runTask(() => { + root.render(); + }); - Fantom.runOnUIThread(() => { - Fantom.enqueueNativeEvent(element, 'change', { - text: 'Hello World', + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + 'Create {type: "AndroidTextInput", nativeID: "text-input"}', + 'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}', + 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}', + ]); }); }); - Fantom.runWorkLoop(); + describe('blur()', () => { + it('does NOT dispatch any commands if the input is NOT focused', () => { + const root = Fantom.createRoot(); + const ref = createRef(); - expect(onChange).toHaveBeenCalledTimes(1); - const [entry] = onChange.mock.lastCall; - expect(entry.text).toEqual('Hello World'); - }); -}); + Fantom.runTask(() => { + root.render(); + }); + + root.takeMountingManagerLogs(); + + const instance = nullthrows(ref.current); -describe('onChangeText', () => { - it('delivers onChangeText event', () => { - const root = Fantom.createRoot(); - const nodeRef = createRef(); - const onChangeText = jest.fn(); + Fantom.runTask(() => { + instance.blur(); + }); - Fantom.runTask(() => { - root.render(); + expect(root.takeMountingManagerLogs()).toEqual([]); + }); + + it('does dispatches the blur command if the input is focused', () => { + const root = Fantom.createRoot(); + const ref = createRef(); + + Fantom.runTask(() => { + root.render(); + }); + + const instance = nullthrows(ref.current); + + Fantom.runTask(() => { + instance.focus(); + }); + + root.takeMountingManagerLogs(); + + Fantom.runTask(() => { + instance.blur(); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "blur"}', + ]); + }); }); - const element = ensureInstance(nodeRef.current, ReactNativeElement); + describe('clear()', () => { + it('dispatches the clear command', () => { + const root = Fantom.createRoot(); + const ref = createRef(); - Fantom.runOnUIThread(() => { - Fantom.enqueueNativeEvent(element, 'change', { - text: 'Hello World', + Fantom.runTask(() => { + root.render( + , + ); + }); + + root.takeMountingManagerLogs(); + + const instance = nullthrows(ref.current); + + Fantom.runTask(() => { + instance.clear(); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "setTextAndSelection, args: [0,"",0,0]"}', + ]); }); }); - Fantom.runWorkLoop(); + describe('isFocused()', () => { + it('returns true if the input is focused', () => { + const root = Fantom.createRoot(); + const ref = createRef(); - expect(onChangeText).toHaveBeenCalledTimes(1); - const [entry] = onChangeText.mock.lastCall; - expect(entry).toEqual('Hello World'); - }); -}); + Fantom.runTask(() => { + root.render(); + }); + + const instance = nullthrows(ref.current); + + expect(instance.isFocused()).toBe(false); + + Fantom.runTask(() => { + instance.focus(); + }); -describe('props.selection', () => { - it('the selection is passed to component view by command', () => { - const root = Fantom.createRoot(); + expect(instance.isFocused()).toBe(true); - Fantom.runTask(() => { - root.render( - - hello World! - , - ); + Fantom.runTask(() => { + instance.blur(); + }); + + expect(instance.isFocused()).toBe(false); + }); }); - expect(root.takeMountingManagerLogs()).toEqual([ - 'Update {type: "RootView", nativeID: (root)}', - 'Create {type: "AndroidTextInput", nativeID: "text-input"}', - 'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}', - 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "setTextAndSelection, args: [0,null,0,4]"}', - ]); + describe('setSelection', () => { + it('dispatches the setTextAndSelection command', () => { + const root = Fantom.createRoot(); + const ref = createRef(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + root.takeMountingManagerLogs(); + + const instance = nullthrows(ref.current); + + Fantom.runTask(() => { + instance.setSelection(2, 5); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "setTextAndSelection, args: [0,null,2,5]"}', + ]); + }); + }); }); });