diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index c0ac2a9f6..2adf0ba78 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -9,6 +9,7 @@ import { View, } from 'react-native'; import { fireEvent, render, screen } from '..'; +import '../matchers/extend-expect'; type OnPressComponentProps = { onPress: () => void; @@ -139,7 +140,6 @@ test('fireEvent.scroll', () => { test('fireEvent.changeText', () => { const onChangeTextMock = jest.fn(); - const CHANGE_TEXT = 'content'; render( @@ -147,9 +147,19 @@ test('fireEvent.changeText', () => { , ); - fireEvent.changeText(screen.getByPlaceholderText('Customer placeholder'), CHANGE_TEXT); + const input = screen.getByPlaceholderText('Customer placeholder'); + fireEvent.changeText(input, 'content'); + expect(onChangeTextMock).toHaveBeenCalledWith('content'); +}); + +it('sets native state value for unmanaged text inputs', () => { + render(); + + const input = screen.getByTestId('input'); + expect(input).toHaveDisplayValue(''); - expect(onChangeTextMock).toHaveBeenCalledWith(CHANGE_TEXT); + fireEvent.changeText(input, 'abc'); + expect(input).toHaveDisplayValue('abc'); }); test('custom component with custom event name', () => { diff --git a/src/cleanup.ts b/src/cleanup.ts index 681d22a47..c7ee13cb5 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -1,11 +1,14 @@ -import * as React from 'react'; +import { clearNativeState } from './native-state'; import { clearRenderResult } from './screen'; -type CleanUpFunction = (nextElement?: React.ReactElement) => void; -let cleanupQueue = new Set(); +type CleanUpFunction = () => void; + +const cleanupQueue = new Set(); export default function cleanup() { + clearNativeState(); clearRenderResult(); + cleanupQueue.forEach((fn) => fn()); cleanupQueue.clear(); } diff --git a/src/fire-event.ts b/src/fire-event.ts index 6859857ff..849c01eea 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -12,6 +12,7 @@ import { isHostTextInput } from './helpers/host-component-names'; import { isPointerEventEnabled } from './helpers/pointer-events'; import { isTextInputEditable } from './helpers/text-input'; import { StringWithAutocomplete } from './types'; +import { nativeState } from './native-state'; type EventHandler = (...args: unknown[]) => unknown; @@ -120,6 +121,8 @@ type EventName = StringWithAutocomplete< >; function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: unknown[]) { + setNativeStateIfNeeded(element, eventName, data[0]); + const handler = findEventHandler(element, eventName); if (!handler) { return; @@ -143,3 +146,14 @@ fireEvent.scroll = (element: ReactTestInstance, ...data: unknown[]) => fireEvent(element, 'scroll', ...data); export default fireEvent; + +function setNativeStateIfNeeded(element: ReactTestInstance, eventName: string, value: unknown) { + if ( + eventName === 'changeText' && + typeof value === 'string' && + isHostTextInput(element) && + isTextInputEditable(element) + ) { + nativeState?.elementValues.set(element, value); + } +} diff --git a/src/helpers/text-input.ts b/src/helpers/text-input.ts index 1a1158369..eaa10f7b9 100644 --- a/src/helpers/text-input.ts +++ b/src/helpers/text-input.ts @@ -1,4 +1,5 @@ import { ReactTestInstance } from 'react-test-renderer'; +import { nativeState } from '../native-state'; import { isHostTextInput } from './host-component-names'; export function isTextInputEditable(element: ReactTestInstance) { @@ -14,5 +15,10 @@ export function getTextInputValue(element: ReactTestInstance) { throw new Error(`Element is not a "TextInput", but it has type "${element.type}".`); } - return element.props.value ?? element.props.defaultValue; + return ( + element.props.value ?? + nativeState?.elementValues.get(element) ?? + element.props.defaultValue ?? + '' + ); } diff --git a/src/native-state.ts b/src/native-state.ts new file mode 100644 index 000000000..48793fd31 --- /dev/null +++ b/src/native-state.ts @@ -0,0 +1,22 @@ +import { ReactTestInstance } from 'react-test-renderer'; + +/** + * Simulated native state for unmanaged controls. + * + * Values from `value` props (managed controls) should take precedence over these values. + */ +export type NativeState = { + elementValues: WeakMap; +}; + +export let nativeState: NativeState | null = null; + +export function initNativeState(): void { + nativeState = { + elementValues: new WeakMap(), + }; +} + +export function clearNativeState(): void { + nativeState = null; +} diff --git a/src/render.tsx b/src/render.tsx index 69d27b53e..dc9335b49 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -12,6 +12,7 @@ import { validateStringsRenderedWithinText } from './helpers/string-validation'; import { renderWithAct } from './render-act'; import { setRenderResult } from './screen'; import { getQueriesForElement } from './within'; +import { initNativeState } from './native-state'; export interface RenderOptions { wrapper?: React.ComponentType; @@ -127,6 +128,8 @@ function buildRenderResult( }); setRenderResult(result); + initNativeState(); + return result; } diff --git a/src/user-event/__tests__/clear.test.tsx b/src/user-event/__tests__/clear.test.tsx index b0fe11c34..27d713a1d 100644 --- a/src/user-event/__tests__/clear.test.tsx +++ b/src/user-event/__tests__/clear.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { TextInput, TextInputProps, View } from 'react-native'; import { createEventLogger, getEventsNames } from '../../test-utils'; import { render, userEvent, screen } from '../..'; +import '../../matchers/extend-expect'; beforeEach(() => { jest.useRealTimers(); @@ -205,4 +206,16 @@ describe('clear()', () => { await user.clear(screen.getByTestId('input')); expect(parentHandler).not.toHaveBeenCalled(); }); + + it('sets native state value for unmanaged text inputs', async () => { + render(); + + const user = userEvent.setup(); + const input = screen.getByTestId('input'); + await user.type(input, 'abc'); + expect(input).toHaveDisplayValue('abc'); + + await user.clear(input); + expect(input).toHaveDisplayValue(''); + }); }); diff --git a/src/user-event/__tests__/paste.test.tsx b/src/user-event/__tests__/paste.test.tsx index 1c01c8b0b..702edbb21 100644 --- a/src/user-event/__tests__/paste.test.tsx +++ b/src/user-event/__tests__/paste.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { TextInput, TextInputProps, View } from 'react-native'; import { createEventLogger, getEventsNames } from '../../test-utils'; import { render, userEvent, screen } from '../..'; +import '../../matchers/extend-expect'; beforeEach(() => { jest.useRealTimers(); @@ -221,4 +222,15 @@ describe('paste()', () => { await user.paste(screen.getByTestId('input'), 'Hi!'); expect(parentHandler).not.toHaveBeenCalled(); }); + + it('sets native state value for unmanaged text inputs', async () => { + render(); + + const user = userEvent.setup(); + const input = screen.getByTestId('input'); + expect(input).toHaveDisplayValue(''); + + await user.paste(input, 'abc'); + expect(input).toHaveDisplayValue('abc'); + }); }); diff --git a/src/user-event/paste.ts b/src/user-event/paste.ts index 80ed6b2bd..ccdaff713 100644 --- a/src/user-event/paste.ts +++ b/src/user-event/paste.ts @@ -3,6 +3,7 @@ import { ErrorWithStack } from '../helpers/errors'; import { isHostTextInput } from '../helpers/host-component-names'; import { isPointerEventEnabled } from '../helpers/pointer-events'; import { isTextInputEditable } from '../helpers/text-input'; +import { nativeState } from '../native-state'; import { EventBuilder } from './event-builder'; import { UserEventInstance } from './setup'; import { dispatchEvent, getTextContentSize, wait } from './utils'; @@ -32,6 +33,7 @@ export async function paste( dispatchEvent(element, 'selectionChange', EventBuilder.TextInput.selectionChange(rangeToClear)); // 3. Paste the text + nativeState?.elementValues.set(element, text); dispatchEvent(element, 'change', EventBuilder.TextInput.change(text)); dispatchEvent(element, 'changeText', text); diff --git a/src/user-event/type/__tests__/type.test.tsx b/src/user-event/type/__tests__/type.test.tsx index 0ded57f86..0be7f35fa 100644 --- a/src/user-event/type/__tests__/type.test.tsx +++ b/src/user-event/type/__tests__/type.test.tsx @@ -3,6 +3,7 @@ import { TextInput, TextInputProps, View } from 'react-native'; import { createEventLogger, getEventsNames, lastEventPayload } from '../../../test-utils'; import { render, screen } from '../../..'; import { userEvent } from '../..'; +import '../../../matchers/extend-expect'; beforeEach(() => { jest.useRealTimers(); @@ -372,4 +373,15 @@ describe('type()', () => { }, }); }); + + it('sets native state value for unmanaged text inputs', async () => { + render(); + + const user = userEvent.setup(); + const input = screen.getByTestId('input'); + expect(input).toHaveDisplayValue(''); + + await user.type(input, 'abc'); + expect(input).toHaveDisplayValue('abc'); + }); }); diff --git a/src/user-event/type/type.ts b/src/user-event/type/type.ts index dbf351fee..7d3a8e6d6 100644 --- a/src/user-event/type/type.ts +++ b/src/user-event/type/type.ts @@ -1,5 +1,6 @@ import { ReactTestInstance } from 'react-test-renderer'; import { isHostTextInput } from '../../helpers/host-component-names'; +import { nativeState } from '../../native-state'; import { EventBuilder } from '../event-builder'; import { ErrorWithStack } from '../../helpers/errors'; import { isTextInputEditable } from '../../helpers/text-input'; @@ -94,6 +95,7 @@ export async function emitTypingEvents( return; } + nativeState?.elementValues.set(element, text); dispatchEvent(element, 'change', EventBuilder.TextInput.change(text)); dispatchEvent(element, 'changeText', text);