Skip to content

Commit 4112abc

Browse files
feat: simulated native TextInput state (#1653)
* feat: basic text input state * refactor: self code review * refactor: code review changes
1 parent 899de72 commit 4112abc

File tree

11 files changed

+106
-7
lines changed

11 files changed

+106
-7
lines changed

src/__tests__/fire-event.test.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
View,
1010
} from 'react-native';
1111
import { fireEvent, render, screen } from '..';
12+
import '../matchers/extend-expect';
1213

1314
type OnPressComponentProps = {
1415
onPress: () => void;
@@ -139,17 +140,26 @@ test('fireEvent.scroll', () => {
139140

140141
test('fireEvent.changeText', () => {
141142
const onChangeTextMock = jest.fn();
142-
const CHANGE_TEXT = 'content';
143143

144144
render(
145145
<View>
146146
<TextInput placeholder="Customer placeholder" onChangeText={onChangeTextMock} />
147147
</View>,
148148
);
149149

150-
fireEvent.changeText(screen.getByPlaceholderText('Customer placeholder'), CHANGE_TEXT);
150+
const input = screen.getByPlaceholderText('Customer placeholder');
151+
fireEvent.changeText(input, 'content');
152+
expect(onChangeTextMock).toHaveBeenCalledWith('content');
153+
});
154+
155+
it('sets native state value for unmanaged text inputs', () => {
156+
render(<TextInput testID="input" />);
157+
158+
const input = screen.getByTestId('input');
159+
expect(input).toHaveDisplayValue('');
151160

152-
expect(onChangeTextMock).toHaveBeenCalledWith(CHANGE_TEXT);
161+
fireEvent.changeText(input, 'abc');
162+
expect(input).toHaveDisplayValue('abc');
153163
});
154164

155165
test('custom component with custom event name', () => {

src/cleanup.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import * as React from 'react';
1+
import { clearNativeState } from './native-state';
22
import { clearRenderResult } from './screen';
33

4-
type CleanUpFunction = (nextElement?: React.ReactElement<any>) => void;
5-
let cleanupQueue = new Set<CleanUpFunction>();
4+
type CleanUpFunction = () => void;
5+
6+
const cleanupQueue = new Set<CleanUpFunction>();
67

78
export default function cleanup() {
9+
clearNativeState();
810
clearRenderResult();
11+
912
cleanupQueue.forEach((fn) => fn());
1013
cleanupQueue.clear();
1114
}

src/fire-event.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { isHostTextInput } from './helpers/host-component-names';
1212
import { isPointerEventEnabled } from './helpers/pointer-events';
1313
import { isTextInputEditable } from './helpers/text-input';
1414
import { StringWithAutocomplete } from './types';
15+
import { nativeState } from './native-state';
1516

1617
type EventHandler = (...args: unknown[]) => unknown;
1718

@@ -120,6 +121,8 @@ type EventName = StringWithAutocomplete<
120121
>;
121122

122123
function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: unknown[]) {
124+
setNativeStateIfNeeded(element, eventName, data[0]);
125+
123126
const handler = findEventHandler(element, eventName);
124127
if (!handler) {
125128
return;
@@ -143,3 +146,14 @@ fireEvent.scroll = (element: ReactTestInstance, ...data: unknown[]) =>
143146
fireEvent(element, 'scroll', ...data);
144147

145148
export default fireEvent;
149+
150+
function setNativeStateIfNeeded(element: ReactTestInstance, eventName: string, value: unknown) {
151+
if (
152+
eventName === 'changeText' &&
153+
typeof value === 'string' &&
154+
isHostTextInput(element) &&
155+
isTextInputEditable(element)
156+
) {
157+
nativeState?.elementValues.set(element, value);
158+
}
159+
}

src/helpers/text-input.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ReactTestInstance } from 'react-test-renderer';
2+
import { nativeState } from '../native-state';
23
import { isHostTextInput } from './host-component-names';
34

45
export function isTextInputEditable(element: ReactTestInstance) {
@@ -14,5 +15,10 @@ export function getTextInputValue(element: ReactTestInstance) {
1415
throw new Error(`Element is not a "TextInput", but it has type "${element.type}".`);
1516
}
1617

17-
return element.props.value ?? element.props.defaultValue;
18+
return (
19+
element.props.value ??
20+
nativeState?.elementValues.get(element) ??
21+
element.props.defaultValue ??
22+
''
23+
);
1824
}

src/native-state.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ReactTestInstance } from 'react-test-renderer';
2+
3+
/**
4+
* Simulated native state for unmanaged controls.
5+
*
6+
* Values from `value` props (managed controls) should take precedence over these values.
7+
*/
8+
export type NativeState = {
9+
elementValues: WeakMap<ReactTestInstance, string>;
10+
};
11+
12+
export let nativeState: NativeState | null = null;
13+
14+
export function initNativeState(): void {
15+
nativeState = {
16+
elementValues: new WeakMap(),
17+
};
18+
}
19+
20+
export function clearNativeState(): void {
21+
nativeState = null;
22+
}

src/render.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { validateStringsRenderedWithinText } from './helpers/string-validation';
1212
import { renderWithAct } from './render-act';
1313
import { setRenderResult } from './screen';
1414
import { getQueriesForElement } from './within';
15+
import { initNativeState } from './native-state';
1516

1617
export interface RenderOptions {
1718
wrapper?: React.ComponentType<any>;
@@ -127,6 +128,8 @@ function buildRenderResult(
127128
});
128129

129130
setRenderResult(result);
131+
initNativeState();
132+
130133
return result;
131134
}
132135

src/user-event/__tests__/clear.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import { TextInput, TextInputProps, View } from 'react-native';
33
import { createEventLogger, getEventsNames } from '../../test-utils';
44
import { render, userEvent, screen } from '../..';
5+
import '../../matchers/extend-expect';
56

67
beforeEach(() => {
78
jest.useRealTimers();
@@ -205,4 +206,16 @@ describe('clear()', () => {
205206
await user.clear(screen.getByTestId('input'));
206207
expect(parentHandler).not.toHaveBeenCalled();
207208
});
209+
210+
it('sets native state value for unmanaged text inputs', async () => {
211+
render(<TextInput testID="input" />);
212+
213+
const user = userEvent.setup();
214+
const input = screen.getByTestId('input');
215+
await user.type(input, 'abc');
216+
expect(input).toHaveDisplayValue('abc');
217+
218+
await user.clear(input);
219+
expect(input).toHaveDisplayValue('');
220+
});
208221
});

src/user-event/__tests__/paste.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import { TextInput, TextInputProps, View } from 'react-native';
33
import { createEventLogger, getEventsNames } from '../../test-utils';
44
import { render, userEvent, screen } from '../..';
5+
import '../../matchers/extend-expect';
56

67
beforeEach(() => {
78
jest.useRealTimers();
@@ -221,4 +222,15 @@ describe('paste()', () => {
221222
await user.paste(screen.getByTestId('input'), 'Hi!');
222223
expect(parentHandler).not.toHaveBeenCalled();
223224
});
225+
226+
it('sets native state value for unmanaged text inputs', async () => {
227+
render(<TextInput testID="input" />);
228+
229+
const user = userEvent.setup();
230+
const input = screen.getByTestId('input');
231+
expect(input).toHaveDisplayValue('');
232+
233+
await user.paste(input, 'abc');
234+
expect(input).toHaveDisplayValue('abc');
235+
});
224236
});

src/user-event/paste.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ErrorWithStack } from '../helpers/errors';
33
import { isHostTextInput } from '../helpers/host-component-names';
44
import { isPointerEventEnabled } from '../helpers/pointer-events';
55
import { isTextInputEditable } from '../helpers/text-input';
6+
import { nativeState } from '../native-state';
67
import { EventBuilder } from './event-builder';
78
import { UserEventInstance } from './setup';
89
import { dispatchEvent, getTextContentSize, wait } from './utils';
@@ -32,6 +33,7 @@ export async function paste(
3233
dispatchEvent(element, 'selectionChange', EventBuilder.TextInput.selectionChange(rangeToClear));
3334

3435
// 3. Paste the text
36+
nativeState?.elementValues.set(element, text);
3537
dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
3638
dispatchEvent(element, 'changeText', text);
3739

src/user-event/type/__tests__/type.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { TextInput, TextInputProps, View } from 'react-native';
33
import { createEventLogger, getEventsNames, lastEventPayload } from '../../../test-utils';
44
import { render, screen } from '../../..';
55
import { userEvent } from '../..';
6+
import '../../../matchers/extend-expect';
67

78
beforeEach(() => {
89
jest.useRealTimers();
@@ -372,4 +373,15 @@ describe('type()', () => {
372373
},
373374
});
374375
});
376+
377+
it('sets native state value for unmanaged text inputs', async () => {
378+
render(<TextInput testID="input" />);
379+
380+
const user = userEvent.setup();
381+
const input = screen.getByTestId('input');
382+
expect(input).toHaveDisplayValue('');
383+
384+
await user.type(input, 'abc');
385+
expect(input).toHaveDisplayValue('abc');
386+
});
375387
});

src/user-event/type/type.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { isHostTextInput } from '../../helpers/host-component-names';
3+
import { nativeState } from '../../native-state';
34
import { EventBuilder } from '../event-builder';
45
import { ErrorWithStack } from '../../helpers/errors';
56
import { isTextInputEditable } from '../../helpers/text-input';
@@ -94,6 +95,7 @@ export async function emitTypingEvents(
9495
return;
9596
}
9697

98+
nativeState?.elementValues.set(element, text);
9799
dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
98100
dispatchEvent(element, 'changeText', text);
99101

0 commit comments

Comments
 (0)