Skip to content

Commit 1391c34

Browse files
committed
feat: userEvent type()
chore: restore tests chore: more tests chore: improve coverage chore: moar tests docs: improve the docs chore: fix lint refactor: code review changes chore: fix lint chore: tweak return type refactor: flush microtasks after event refactor: re-organize waits refactor: improve dispatch event functions refactor: remove flushMicroTasks calls
1 parent 5bb5d2d commit 1391c34

27 files changed

+2011
-118
lines changed

experiments-app/src/screens/TextInputEvents.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ const handlePressOut = buildEventLogger('pressOut');
77
const handleFocus = buildEventLogger('focus');
88
const handleBlur = buildEventLogger('blur');
99
const handleChange = buildEventLogger('change');
10+
const handleEndEditing = buildEventLogger('endEditing');
1011
const handleSubmitEditing = buildEventLogger('submitEditing');
12+
const handleKeyPress = buildEventLogger('keyPress');
13+
const handleTextInput = buildEventLogger('textInput');
14+
const handleSelectionChange = buildEventLogger('selectionChange');
15+
const handleContentSizeChange = buildEventLogger('contentSizeChange');
1116

1217
export function TextInputEvents() {
1318
const [value, setValue] = React.useState('');
@@ -29,7 +34,12 @@ export function TextInputEvents() {
2934
onFocus={handleFocus}
3035
onBlur={handleBlur}
3136
onChange={handleChange}
37+
onEndEditing={handleEndEditing}
3238
onSubmitEditing={handleSubmitEditing}
39+
onKeyPress={handleKeyPress}
40+
onTextInput={handleTextInput}
41+
onSelectionChange={handleSelectionChange}
42+
onContentSizeChange={handleContentSizeChange}
3343
/>
3444
</SafeAreaView>
3545
);

src/__tests__/act.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,7 @@ test('should be able to await act', async () => {
5050
const result = await act(async () => {});
5151
expect(result).toBe(undefined);
5252
});
53+
54+
test('should be able to await act when promise rejects', async () => {
55+
await expect(act(async () => Promise.reject('error'))).rejects.toBe('error');
56+
});

src/fireEvent.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import act from './act';
3-
import { isHostElement } from './helpers/component-tree';
4-
import { getHostComponentNames } from './helpers/host-component-names';
53
import { isPointerEventEnabled } from './helpers/pointer-events';
4+
import { isHostElement } from './helpers/component-tree';
5+
import { isHostTextInput } from './helpers/host-component-names';
66

77
type EventHandler = (...args: unknown[]) => unknown;
88

9-
const isHostTextInput = (element?: ReactTestInstance) => {
10-
return element?.type === getHostComponentNames().textInput;
11-
};
12-
139
export function isTouchResponder(element: ReactTestInstance) {
1410
if (!isHostElement(element)) {
1511
return false;

src/helpers/accessiblity.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {
44
StyleSheet,
55
} from 'react-native';
66
import { ReactTestInstance } from 'react-test-renderer';
7-
import { getConfig } from '../config';
87
import { getHostSiblings } from './component-tree';
8+
import { getHostComponentNames } from './host-component-names';
99

1010
type IsInaccessibleOptions = {
1111
cache?: WeakMap<ReactTestInstance, boolean>;
@@ -99,8 +99,7 @@ export function isAccessibilityElement(
9999
return element.props.accessible;
100100
}
101101

102-
const hostComponentNames = getConfig().hostComponentNames;
103-
102+
const hostComponentNames = getHostComponentNames();
104103
return (
105104
element?.type === hostComponentNames?.text ||
106105
element?.type === hostComponentNames?.textInput ||

src/helpers/deprecation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function deprecateQuery<QueryFn extends (...args: any) => any>(
3737

3838
const warned: { [functionName: string]: boolean } = {};
3939

40-
// istambul ignore next: Occasionally used
40+
/* istanbul ignore next: occasionally used */
4141
export function printDeprecationWarning(functionName: string) {
4242
if (warned[functionName]) {
4343
return;

src/helpers/host-component-names.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,7 @@ function getByTestId(instance: ReactTestInstance, testID: string) {
6565

6666
return nodes[0];
6767
}
68+
69+
export function isHostTextInput(element?: ReactTestInstance) {
70+
return element?.type === getHostComponentNames().textInput;
71+
}

src/user-event/event-builder/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { CommonEventBuilder } from './common';
2+
import { TextInputEventBuilder } from './text-input';
23

34
export const EventBuilder = {
45
Common: CommonEventBuilder,
6+
TextInput: TextInputEventBuilder,
57
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { ContentSize } from '../utils/content-size';
2+
import { TextRange } from '../utils/text-range';
3+
4+
export const TextInputEventBuilder = {
5+
/**
6+
* Experimental values:
7+
* - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
8+
* - Android: `{"eventCount": 6, "target": 53, "text": "Tes"}`
9+
*/
10+
change: (text: string) => {
11+
return {
12+
nativeEvent: { text, target: 0, eventCount: 0 },
13+
};
14+
},
15+
16+
/**
17+
* Experimental values:
18+
* - iOS: `{"eventCount": 3, "key": "a", "target": 75}`
19+
* - Android: `{"key": "a"}`
20+
*/
21+
keyPress: (key: string) => {
22+
return {
23+
nativeEvent: { key },
24+
};
25+
},
26+
27+
/**
28+
* Experimental values:
29+
* - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
30+
* - Android: `{"target": 53, "text": "Test"}`
31+
*/
32+
submitEditing: (text: string) => {
33+
return {
34+
nativeEvent: { text, target: 0 },
35+
};
36+
},
37+
38+
/**
39+
* Experimental values:
40+
* - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
41+
* - Android: `{"target": 53, "text": "Test"}`
42+
*/
43+
endEditing: (text: string) => {
44+
return {
45+
nativeEvent: { text, target: 0 },
46+
};
47+
},
48+
49+
/**
50+
* Experimental values:
51+
* - iOS: `{"selection": {"end": 4, "start": 4}, "target": 75}`
52+
* - Android: `{"selection": {"end": 4, "start": 4}}`
53+
*/
54+
selectionChange: ({ start, end }: TextRange) => {
55+
return {
56+
nativeEvent: { selection: { start, end } },
57+
};
58+
},
59+
60+
/**
61+
* Experimental values:
62+
* - iOS: `{"eventCount": 2, "previousText": "Te", "range": {"end": 2, "start": 2}, "target": 75, "text": "s"}`
63+
* - Android: `{"previousText": "Te", "range": {"end": 2, "start": 0}, "target": 53, "text": "Tes"}`
64+
*/
65+
textInput: (text: string, previousText: string) => {
66+
return {
67+
nativeEvent: {
68+
text,
69+
previousText,
70+
range: { start: text.length, end: text.length },
71+
target: 0,
72+
},
73+
};
74+
},
75+
76+
/**
77+
* Experimental values:
78+
* - iOS: `{"contentSize": {"height": 21.666666666666668, "width": 11.666666666666666}, "target": 75}`
79+
* - Android: `{"contentSize": {"height": 61.45454406738281, "width": 352.7272644042969}, "target": 53}`
80+
*/
81+
contentSizeChange: ({ width, height }: ContentSize) => {
82+
return {
83+
nativeEvent: { contentSize: { width, height }, target: 0 },
84+
};
85+
},
86+
};

src/user-event/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { setup } from './setup';
3-
import { PressOptions } from './press/press';
3+
import { PressOptions } from './press';
4+
import { TypeOptions } from './type';
45

56
export const userEvent = {
67
setup,
@@ -9,6 +10,6 @@ export const userEvent = {
910
press: (element: ReactTestInstance) => setup().press(element),
1011
longPress: (element: ReactTestInstance, options?: PressOptions) =>
1112
setup().longPress(element, options),
12-
type: (element: ReactTestInstance, text: string) =>
13-
setup().type(element, text),
13+
type: (element: ReactTestInstance, text: string, options?: TypeOptions) =>
14+
setup().type(element, text, options),
1415
};

src/user-event/press/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { press, longPress } from './press';
1+
export { PressOptions, press, longPress } from './press';

src/user-event/setup/setup.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { jestFakeTimersAreEnabled } from '../../helpers/timers';
3-
import { press, longPress } from '../press';
4-
import { type } from '../type';
5-
import { PressOptions } from '../press/press';
3+
import { PressOptions, press, longPress } from '../press';
4+
import { TypeOptions, type } from '../type';
65

76
export interface UserEventSetupOptions {
87
/**
@@ -46,14 +45,20 @@ const defaultOptions: Required<UserEventSetupOptions> = {
4645
* Creates a new instance of user event instance with the given options.
4746
*
4847
* @param options
49-
* @returns
48+
* @returns UserEvent instance
5049
*/
5150
export function setup(options?: UserEventSetupOptions) {
5251
const config = createConfig(options);
5352
const instance = createInstance(config);
5453
return instance;
5554
}
5655

56+
/**
57+
* Options affecting all user event interactions.
58+
*
59+
* @param delay between some subsequent inputs like typing a series of characters
60+
* @param advanceTimers function to be called to advance fake timers
61+
*/
5762
export interface UserEventConfig {
5863
delay: number;
5964
advanceTimers: (delay: number) => Promise<void> | void;
@@ -66,14 +71,43 @@ function createConfig(options?: UserEventSetupOptions): UserEventConfig {
6671
};
6772
}
6873

74+
/**
75+
* UserEvent instance used to invoke user interaction functions.
76+
*/
6977
export interface UserEventInstance {
7078
config: UserEventConfig;
79+
7180
press: (element: ReactTestInstance) => Promise<void>;
7281
longPress: (
7382
element: ReactTestInstance,
7483
options?: PressOptions
7584
) => Promise<void>;
76-
type: (element: ReactTestInstance, text: string) => Promise<void>;
85+
86+
/**
87+
* Simulate user pressing on given `TextInput` element and typing given text.
88+
*
89+
* This method will trigger the events for each character of the text:
90+
* `keyPress`, `change`, `changeText`, `endEditing`, etc.
91+
*
92+
* It will also trigger events connected with entering and leaving the text
93+
* input.
94+
*
95+
* The exact events sent depend on the props of TextInput (`editable`,
96+
* `multiline`, value, defaultValue, etc) and passed options.
97+
*
98+
* @param element TextInput element to type on
99+
* @param text Text to type
100+
* @param options Options affecting typing behavior:
101+
* - `skipPress` - if true, `pressIn` and `pressOut` events will not be
102+
* triggered.
103+
* - `submitEditing` - if true, `submitEditing` event will be triggered after
104+
* typing the text.
105+
*/
106+
type: (
107+
element: ReactTestInstance,
108+
text: string,
109+
options?: TypeOptions
110+
) => Promise<void>;
77111
}
78112

79113
function createInstance(config: UserEventConfig): UserEventInstance {

0 commit comments

Comments
 (0)