Skip to content

Commit 574205a

Browse files
feat: User Event type() (#1396)
* 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 * chore: update snapshots * chore: do not expose User Event docs yet * refactor: simplify dispatchEvent code * refactor: press * refactor: move warn method up * refactor: finishing touches * docs: correct typos
1 parent b2d642f commit 574205a

30 files changed

+2025
-183
lines changed

experiments-app/src/screens/TextInputEvents.tsx

+10
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

+4
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

+2-6
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,12 @@ import {
77
ScrollViewProps,
88
} from 'react-native';
99
import act from './act';
10-
import { isHostElement } from './helpers/component-tree';
11-
import { getHostComponentNames } from './helpers/host-component-names';
1210
import { isPointerEventEnabled } from './helpers/pointer-events';
11+
import { isHostElement } from './helpers/component-tree';
12+
import { isHostTextInput } from './helpers/host-component-names';
1313

1414
type EventHandler = (...args: unknown[]) => unknown;
1515

16-
const isHostTextInput = (element?: ReactTestInstance) => {
17-
return element?.type === getHostComponentNames().textInput;
18-
};
19-
2016
export function isTouchResponder(element: ReactTestInstance) {
2117
if (!isHostElement(element)) {
2218
return false;

src/helpers/deprecation.ts

+1-1
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

+8
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,11 @@ function getByTestId(instance: ReactTestInstance, testID: string) {
6565

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

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

+35-19
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
1+
/**
2+
* Experimental values:
3+
* - iOS: `{"changedTouches": [[Circular]], "identifier": 1, "locationX": 253, "locationY": 30.333328247070312, "pageX": 273, "pageY": 141.3333282470703, "target": 75, "timestamp": 875928682.0450834, "touches": [[Circular]]}`
4+
* - Android: `{"changedTouches": [[Circular]], "identifier": 0, "locationX": 160, "locationY": 40.3636360168457, "pageX": 180, "pageY": 140.36363220214844, "target": 53, "targetSurface": -1, "timestamp": 10290805, "touches": [[Circular]]}`
5+
*/
6+
function touch() {
7+
return {
8+
persist: jest.fn(),
9+
currentTarget: { measure: jest.fn() },
10+
nativeEvent: {
11+
changedTouches: [],
12+
identifier: 0,
13+
locationX: 0,
14+
locationY: 0,
15+
pageX: 0,
16+
pageY: 0,
17+
target: 0,
18+
timestamp: Date.now(),
19+
touches: [],
20+
},
21+
};
22+
}
23+
124
export const CommonEventBuilder = {
2-
/**
3-
* Experimental values:
4-
* - iOS: `{"changedTouches": [[Circular]], "identifier": 1, "locationX": 253, "locationY": 30.333328247070312, "pageX": 273, "pageY": 141.3333282470703, "target": 75, "timestamp": 875928682.0450834, "touches": [[Circular]]}`
5-
* - Android: `{"changedTouches": [[Circular]], "identifier": 0, "locationX": 160, "locationY": 40.3636360168457, "pageX": 180, "pageY": 140.36363220214844, "target": 53, "targetSurface": -1, "timestamp": 10290805, "touches": [[Circular]]}`
6-
*/
7-
touch: () => {
25+
touch,
26+
27+
responderGrant: () => {
828
return {
9-
persist: jest.fn(),
10-
currentTarget: { measure: jest.fn() },
11-
nativeEvent: {
12-
changedTouches: [],
13-
identifier: 0,
14-
locationX: 0,
15-
locationY: 0,
16-
pageX: 0,
17-
pageY: 0,
18-
target: 0,
19-
timestamp: Date.now(),
20-
touches: [],
21-
},
29+
...touch(),
30+
dispatchConfig: { registrationName: 'onResponderGrant' },
31+
};
32+
},
33+
34+
responderRelease: () => {
35+
return {
36+
...touch(),
37+
dispatchConfig: { registrationName: 'onResponderRelease' },
2238
};
2339
},
2440

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

+2
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
};
+86
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

+4-3
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/__tests__/longPress.real-timers.test.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import React from 'react';
22
import { Pressable, Text } from 'react-native';
33
import { render, screen } from '../../../pure';
44
import { userEvent } from '../..';
5-
import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers';
5+
import * as WarnAboutRealTimers from '../../utils/warn-about-real-timers';
66

77
describe('userEvent.longPress with real timers', () => {
88
beforeEach(() => {
99
jest.useRealTimers();
1010
jest.restoreAllMocks();
11-
jest.spyOn(WarnAboutRealTimers, 'warnAboutRealTimers').mockImplementation();
11+
jest
12+
.spyOn(WarnAboutRealTimers, 'warnAboutRealTimersIfNeeded')
13+
.mockImplementation();
1214
});
1315

1416
test('calls onLongPress if the delayLongPress is the default one', async () => {

src/user-event/press/__tests__/press.real-timers.test.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import {
1010
import { createEventLogger, getEventsName } from '../../../test-utils';
1111
import { render, screen } from '../../..';
1212
import { userEvent } from '../..';
13-
import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers';
13+
import * as WarnAboutRealTimers from '../../utils/warn-about-real-timers';
1414

1515
describe('userEvent.press with real timers', () => {
1616
beforeEach(() => {
1717
jest.useRealTimers();
1818
jest.restoreAllMocks();
19-
jest.spyOn(WarnAboutRealTimers, 'warnAboutRealTimers').mockImplementation();
19+
jest
20+
.spyOn(WarnAboutRealTimers, 'warnAboutRealTimersIfNeeded')
21+
.mockImplementation();
2022
});
2123

2224
test('calls onPressIn, onPress and onPressOut prop of touchable', async () => {

src/user-event/press/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { press, longPress } from './press';
1+
export { PressOptions, press, longPress } from './press';

0 commit comments

Comments
 (0)