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);