@testing-library/react-native 12.1.3 → 12.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/.eslintcache +1 -1
  2. package/.eslintignore +1 -0
  3. package/build/fireEvent.js +2 -5
  4. package/build/fireEvent.js.map +1 -1
  5. package/build/helpers/component-tree.d.ts +11 -5
  6. package/build/helpers/component-tree.js +5 -1
  7. package/build/helpers/component-tree.js.map +1 -1
  8. package/build/helpers/deprecation.js +1 -1
  9. package/build/helpers/deprecation.js.map +1 -1
  10. package/build/helpers/findAll.d.ts +2 -1
  11. package/build/helpers/findAll.js +2 -1
  12. package/build/helpers/findAll.js.map +1 -1
  13. package/build/helpers/host-component-names.d.ts +12 -0
  14. package/build/helpers/host-component-names.js +18 -0
  15. package/build/helpers/host-component-names.js.map +1 -1
  16. package/build/helpers/matchers/matchLabelText.js +1 -1
  17. package/build/helpers/matchers/matchLabelText.js.map +1 -1
  18. package/build/pure.d.ts +2 -0
  19. package/build/pure.js +7 -0
  20. package/build/pure.js.map +1 -1
  21. package/build/queries/a11yState.js +1 -1
  22. package/build/queries/a11yState.js.map +1 -1
  23. package/build/queries/a11yValue.js +1 -1
  24. package/build/queries/a11yValue.js.map +1 -1
  25. package/build/queries/displayValue.js +5 -6
  26. package/build/queries/displayValue.js.map +1 -1
  27. package/build/queries/hintText.js +1 -1
  28. package/build/queries/hintText.js.map +1 -1
  29. package/build/queries/labelText.js +1 -1
  30. package/build/queries/labelText.js.map +1 -1
  31. package/build/queries/placeholderText.js +3 -4
  32. package/build/queries/placeholderText.js.map +1 -1
  33. package/build/queries/role.js +1 -1
  34. package/build/queries/role.js.map +1 -1
  35. package/build/queries/testId.js +3 -3
  36. package/build/queries/testId.js.map +1 -1
  37. package/build/queries/text.js +1 -2
  38. package/build/queries/text.js.map +1 -1
  39. package/build/render.js.map +1 -1
  40. package/build/user-event/clear.d.ts +3 -0
  41. package/build/user-event/clear.js +41 -0
  42. package/build/user-event/clear.js.map +1 -0
  43. package/build/user-event/event-builder/common.d.ts +48 -6
  44. package/build/user-event/event-builder/common.js +37 -20
  45. package/build/user-event/event-builder/common.js.map +1 -1
  46. package/build/user-event/event-builder/index.d.ts +94 -0
  47. package/build/user-event/event-builder/index.js +3 -1
  48. package/build/user-event/event-builder/index.js.map +1 -1
  49. package/build/user-event/event-builder/text-input.d.ts +91 -0
  50. package/build/user-event/event-builder/text-input.js +117 -0
  51. package/build/user-event/event-builder/text-input.js.map +1 -0
  52. package/build/user-event/index.d.ts +5 -2
  53. package/build/user-event/index.js +8 -1
  54. package/build/user-event/index.js.map +1 -1
  55. package/build/user-event/press/index.d.ts +1 -1
  56. package/build/user-event/press/index.js +6 -0
  57. package/build/user-event/press/index.js.map +1 -1
  58. package/build/user-event/press/press.d.ts +3 -3
  59. package/build/user-event/press/press.js +54 -64
  60. package/build/user-event/press/press.js.map +1 -1
  61. package/build/user-event/setup/setup.d.ts +45 -3
  62. package/build/user-event/setup/setup.js +17 -2
  63. package/build/user-event/setup/setup.js.map +1 -1
  64. package/build/user-event/type/index.d.ts +1 -1
  65. package/build/user-event/type/index.js +6 -0
  66. package/build/user-event/type/index.js.map +1 -1
  67. package/build/user-event/type/parseKeys.d.ts +1 -0
  68. package/build/user-event/type/parseKeys.js +40 -0
  69. package/build/user-event/type/parseKeys.js.map +1 -0
  70. package/build/user-event/type/type.d.ts +7 -2
  71. package/build/user-event/type/type.js +70 -8
  72. package/build/user-event/type/type.js.map +1 -1
  73. package/build/user-event/utils/content-size.d.ts +15 -0
  74. package/build/user-event/utils/content-size.js +26 -0
  75. package/build/user-event/utils/content-size.js.map +1 -0
  76. package/build/user-event/utils/{events.d.ts → dispatch-event.d.ts} +2 -2
  77. package/build/user-event/utils/dispatch-event.js +36 -0
  78. package/build/user-event/utils/dispatch-event.js.map +1 -0
  79. package/build/user-event/utils/host-components.d.ts +2 -0
  80. package/build/user-event/utils/host-components.js +11 -0
  81. package/build/user-event/utils/host-components.js.map +1 -0
  82. package/build/user-event/utils/index.d.ts +5 -1
  83. package/build/user-event/utils/index.js +48 -4
  84. package/build/user-event/utils/index.js.map +1 -1
  85. package/build/user-event/utils/text-range.d.ts +4 -0
  86. package/build/user-event/utils/text-range.js +2 -0
  87. package/build/user-event/utils/text-range.js.map +1 -0
  88. package/build/user-event/utils/warn-about-real-timers.d.ts +1 -0
  89. package/build/user-event/utils/warn-about-real-timers.js +20 -0
  90. package/build/user-event/utils/warn-about-real-timers.js.map +1 -0
  91. package/examples/basic/.expo/README.md +15 -0
  92. package/examples/basic/.expo/packager-info.json +4 -0
  93. package/examples/basic/.expo/settings.json +10 -0
  94. package/examples/basic/__tests__/App.test.tsx +30 -12
  95. package/examples/basic/package.json +7 -7
  96. package/examples/basic/yarn.lock +7499 -0
  97. package/examples/react-navigation/README.md +2 -0
  98. package/examples/react-navigation/package.json +5 -5
  99. package/examples/react-navigation/yarn.lock +5018 -0
  100. package/examples/redux/README.md +5 -0
  101. package/examples/redux/package.json +7 -7
  102. package/examples/redux/yarn.lock +4819 -0
  103. package/experiments-app/.expo/packager-info.json +2 -2
  104. package/experiments-app/package.json +7 -9
  105. package/experiments-app/src/MainScreen.tsx +1 -0
  106. package/experiments-app/src/experiments.ts +20 -2
  107. package/experiments-app/src/screens/FlatListEvents.tsx +57 -0
  108. package/experiments-app/src/screens/ScrollViewEvents.tsx +65 -0
  109. package/experiments-app/src/screens/SectionListEvents.tsx +91 -0
  110. package/experiments-app/src/screens/TextInputEventPropagation.tsx +5 -17
  111. package/experiments-app/src/screens/TextInputEvents.tsx +13 -15
  112. package/experiments-app/src/utils/helpers.ts +13 -3
  113. package/experiments-app/yarn.lock +901 -1105
  114. package/experiments-rtl/.babelrc +8 -0
  115. package/experiments-rtl/.eslintrc.json +3 -0
  116. package/experiments-rtl/.gitignore +35 -0
  117. package/experiments-rtl/README.md +34 -0
  118. package/experiments-rtl/jest-setup.js +1 -0
  119. package/experiments-rtl/jest.config.js +4 -0
  120. package/experiments-rtl/next.config.js +4 -0
  121. package/experiments-rtl/package.json +38 -0
  122. package/experiments-rtl/postcss.config.js +6 -0
  123. package/experiments-rtl/public/next.svg +1 -0
  124. package/experiments-rtl/public/vercel.svg +1 -0
  125. package/experiments-rtl/src/app/__tests__/click.test.tsx +31 -0
  126. package/experiments-rtl/src/app/__tests__/managed-text-input.test.tsx +51 -0
  127. package/experiments-rtl/src/app/globals.css +27 -0
  128. package/experiments-rtl/src/app/layout.tsx +22 -0
  129. package/experiments-rtl/src/app/page.tsx +113 -0
  130. package/experiments-rtl/tailwind.config.ts +20 -0
  131. package/experiments-rtl/tsconfig.json +28 -0
  132. package/experiments-rtl/yarn.lock +5418 -0
  133. package/package.json +4 -2
  134. package/src/__tests__/act.test.tsx +4 -0
  135. package/src/fireEvent.ts +1 -5
  136. package/src/helpers/component-tree.ts +14 -9
  137. package/src/helpers/deprecation.ts +1 -1
  138. package/src/helpers/findAll.ts +6 -4
  139. package/src/helpers/host-component-names.tsx +21 -0
  140. package/src/helpers/matchers/matchLabelText.ts +0 -1
  141. package/src/pure.ts +2 -0
  142. package/src/queries/a11yState.ts +2 -6
  143. package/src/queries/a11yValue.ts +2 -6
  144. package/src/queries/displayValue.ts +7 -14
  145. package/src/queries/hintText.ts +2 -7
  146. package/src/queries/labelText.ts +1 -3
  147. package/src/queries/placeholderText.ts +6 -13
  148. package/src/queries/role.ts +1 -2
  149. package/src/queries/testId.ts +5 -10
  150. package/src/queries/text.ts +3 -6
  151. package/src/render.tsx +1 -1
  152. package/src/user-event/__tests__/__snapshots__/clear.test.tsx.snap +269 -0
  153. package/src/user-event/__tests__/clear.test.tsx +217 -0
  154. package/src/user-event/clear.ts +59 -0
  155. package/src/user-event/event-builder/common.ts +35 -19
  156. package/src/user-event/event-builder/index.ts +2 -0
  157. package/src/user-event/event-builder/text-input.ts +86 -0
  158. package/src/user-event/index.ts +7 -3
  159. package/src/user-event/press/__tests__/longPress.real-timers.test.tsx +4 -2
  160. package/src/user-event/press/__tests__/press.real-timers.test.tsx +4 -2
  161. package/src/user-event/press/__tests__/press.test.tsx +40 -5
  162. package/src/user-event/press/index.ts +1 -1
  163. package/src/user-event/press/press.ts +93 -64
  164. package/src/user-event/setup/setup.ts +54 -5
  165. package/src/user-event/type/__tests__/__snapshots__/type-managed.test.tsx.snap +339 -0
  166. package/src/user-event/type/__tests__/__snapshots__/type.test.tsx.snap +644 -2
  167. package/src/user-event/type/__tests__/parseKeys.test.ts +23 -0
  168. package/src/user-event/type/__tests__/type-managed.test.tsx +120 -0
  169. package/src/user-event/type/__tests__/type.test.tsx +299 -27
  170. package/src/user-event/type/index.ts +1 -1
  171. package/src/user-event/type/parseKeys.ts +41 -0
  172. package/src/user-event/type/type.ts +128 -10
  173. package/src/user-event/utils/__tests__/dispatch-event.test.tsx +41 -0
  174. package/src/user-event/utils/__tests__/wait.test.ts +0 -1
  175. package/src/user-event/utils/content-size.ts +25 -0
  176. package/src/user-event/utils/dispatch-event.ts +38 -0
  177. package/src/user-event/utils/host-components.ts +6 -0
  178. package/src/user-event/utils/index.ts +5 -1
  179. package/src/user-event/utils/text-range.ts +4 -0
  180. package/src/user-event/{press/utils/warnAboutRealTimers.ts → utils/warn-about-real-timers.ts} +8 -1
  181. package/website/docs/API.md +19 -25
  182. package/website/docs/Queries.md +64 -59
  183. package/website/docs/UserEvent.md +134 -9
  184. package/website/sidebars.js +1 -1
  185. package/build/helpers/filterNodeByType.d.ts +0 -3
  186. package/build/helpers/filterNodeByType.js +0 -9
  187. package/build/helpers/filterNodeByType.js.map +0 -1
  188. package/build/user-event/press/utils/warnAboutRealTimers.d.ts +0 -1
  189. package/build/user-event/press/utils/warnAboutRealTimers.js +0 -14
  190. package/build/user-event/press/utils/warnAboutRealTimers.js.map +0 -1
  191. package/build/user-event/utils/events.js +0 -44
  192. package/build/user-event/utils/events.js.map +0 -1
  193. package/src/helpers/filterNodeByType.ts +0 -7
  194. package/src/user-event/utils/events.ts +0 -54
@@ -0,0 +1,120 @@
1
+ import * as React from 'react';
2
+ import { TextInput } from 'react-native';
3
+ import { createEventLogger } from '../../../test-utils/events';
4
+ import { render } from '../../..';
5
+ import { userEvent } from '../..';
6
+
7
+ beforeEach(() => {
8
+ jest.useRealTimers();
9
+ });
10
+
11
+ interface ManagedTextInputProps {
12
+ valueTransformer?: (text: string) => string;
13
+ logEvent: (name: string) => (event: any) => void;
14
+ initialValue?: string;
15
+ }
16
+
17
+ function ManagedTextInput({
18
+ logEvent,
19
+ valueTransformer,
20
+ initialValue = '',
21
+ }: ManagedTextInputProps) {
22
+ const [value, setValue] = React.useState(initialValue);
23
+
24
+ const handleChangeText = (text: string) => {
25
+ logEvent('changeText')(text);
26
+ const newValue = valueTransformer?.(text) ?? text;
27
+ setValue(newValue);
28
+ };
29
+
30
+ return (
31
+ <TextInput
32
+ testID="input"
33
+ value={value}
34
+ onChangeText={handleChangeText}
35
+ onFocus={logEvent('focus')}
36
+ onBlur={logEvent('blur')}
37
+ onPressIn={logEvent('pressIn')}
38
+ onPressOut={logEvent('pressOut')}
39
+ onChange={logEvent('change')}
40
+ onKeyPress={logEvent('keyPress')}
41
+ onTextInput={logEvent('textInput')}
42
+ onSelectionChange={logEvent('selectionChange')}
43
+ onSubmitEditing={logEvent('submitEditing')}
44
+ onEndEditing={logEvent('endEditing')}
45
+ onContentSizeChange={logEvent('contentSizeChange')}
46
+ />
47
+ );
48
+ }
49
+
50
+ describe('type() for managed TextInput', () => {
51
+ it('supports basic case', async () => {
52
+ jest.spyOn(Date, 'now').mockImplementation(() => 100100100100);
53
+ const { events, logEvent } = createEventLogger();
54
+ const screen = render(<ManagedTextInput logEvent={logEvent} />);
55
+
56
+ const user = userEvent.setup();
57
+ await user.type(screen.getByTestId('input'), 'Wow');
58
+
59
+ const eventNames = events.map((e) => e.name);
60
+ expect(eventNames).toEqual([
61
+ 'pressIn',
62
+ 'focus',
63
+ 'pressOut',
64
+ 'keyPress',
65
+ 'change',
66
+ 'changeText',
67
+ 'selectionChange',
68
+ 'keyPress',
69
+ 'change',
70
+ 'changeText',
71
+ 'selectionChange',
72
+ 'keyPress',
73
+ 'change',
74
+ 'changeText',
75
+ 'selectionChange',
76
+ 'endEditing',
77
+ 'blur',
78
+ ]);
79
+
80
+ expect(events).toMatchSnapshot('input: "Wow"');
81
+ });
82
+
83
+ test('supports rejecting TextInput', async () => {
84
+ jest.spyOn(Date, 'now').mockImplementation(() => 100100100100);
85
+ const { events, logEvent } = createEventLogger();
86
+ const screen = render(
87
+ <ManagedTextInput
88
+ initialValue="XXX"
89
+ logEvent={logEvent}
90
+ valueTransformer={() => 'XXX'}
91
+ />
92
+ );
93
+
94
+ const user = userEvent.setup();
95
+ await user.type(screen.getByTestId('input'), 'ABC');
96
+
97
+ const eventNames = events.map((e) => e.name);
98
+ expect(eventNames).toEqual([
99
+ 'pressIn',
100
+ 'focus',
101
+ 'pressOut',
102
+ 'keyPress',
103
+ 'change',
104
+ 'changeText',
105
+ 'selectionChange',
106
+ 'keyPress',
107
+ 'change',
108
+ 'changeText',
109
+ 'selectionChange',
110
+ 'keyPress',
111
+ 'change',
112
+ 'changeText',
113
+ 'selectionChange',
114
+ 'endEditing',
115
+ 'blur',
116
+ ]);
117
+
118
+ expect(events).toMatchSnapshot('input: "ABC", value: "XXX"');
119
+ });
120
+ });
@@ -1,51 +1,317 @@
1
1
  import * as React from 'react';
2
- import { TextInput } from 'react-native';
3
- import { createEventLogger } from '../../../test-utils';
2
+ import { View, TextInput, TextInputProps } from 'react-native';
3
+ import { createEventLogger } from '../../../test-utils/events';
4
4
  import { render } from '../../..';
5
5
  import { userEvent } from '../..';
6
6
 
7
- describe('user.type()', () => {
8
- it('dispatches required events', async () => {
9
- const { events, logEvent } = createEventLogger();
7
+ beforeEach(() => {
8
+ jest.useRealTimers();
9
+ });
10
+
11
+ function renderTextInputWithToolkit(props: TextInputProps = {}) {
12
+ const { events, logEvent } = createEventLogger();
13
+
14
+ const screen = render(
15
+ <TextInput
16
+ testID="input"
17
+ onFocus={logEvent('focus')}
18
+ onBlur={logEvent('blur')}
19
+ onPressIn={logEvent('pressIn')}
20
+ onPressOut={logEvent('pressOut')}
21
+ onChange={logEvent('change')}
22
+ onChangeText={logEvent('changeText')}
23
+ onKeyPress={logEvent('keyPress')}
24
+ onTextInput={logEvent('textInput')}
25
+ onSelectionChange={logEvent('selectionChange')}
26
+ onSubmitEditing={logEvent('submitEditing')}
27
+ onEndEditing={logEvent('endEditing')}
28
+ onContentSizeChange={logEvent('contentSizeChange')}
29
+ {...props}
30
+ />
31
+ );
32
+
33
+ return {
34
+ ...screen,
35
+ events,
36
+ };
37
+ }
38
+
39
+ describe('type()', () => {
40
+ it('supports basic case', async () => {
41
+ jest.spyOn(Date, 'now').mockImplementation(() => 100100100100);
42
+ const { events, ...queries } = renderTextInputWithToolkit();
43
+
10
44
  const user = userEvent.setup();
11
- const screen = render(
12
- <TextInput
13
- testID="input"
14
- onChangeText={logEvent('changeText')}
15
- onFocus={logEvent('focus')}
16
- onBlur={logEvent('blur')}
17
- />
45
+ await user.type(queries.getByTestId('input'), 'abc');
46
+
47
+ const eventNames = events.map((e) => e.name);
48
+ expect(eventNames).toEqual([
49
+ 'pressIn',
50
+ 'focus',
51
+ 'pressOut',
52
+ 'keyPress',
53
+ 'change',
54
+ 'changeText',
55
+ 'selectionChange',
56
+ 'keyPress',
57
+ 'change',
58
+ 'changeText',
59
+ 'selectionChange',
60
+ 'keyPress',
61
+ 'change',
62
+ 'changeText',
63
+ 'selectionChange',
64
+ 'endEditing',
65
+ 'blur',
66
+ ]);
67
+
68
+ expect(events).toMatchSnapshot('input: "abc"');
69
+ });
70
+
71
+ it.each(['modern', 'legacy'])('works with %s fake timers', async (type) => {
72
+ jest.useFakeTimers({ legacyFakeTimers: type === 'legacy' });
73
+ const { events, ...queries } = renderTextInputWithToolkit();
74
+
75
+ const user = userEvent.setup();
76
+ await user.type(queries.getByTestId('input'), 'abc');
77
+
78
+ const eventNames = events.map((e) => e.name);
79
+ expect(eventNames).toEqual([
80
+ 'pressIn',
81
+ 'focus',
82
+ 'pressOut',
83
+ 'keyPress',
84
+ 'change',
85
+ 'changeText',
86
+ 'selectionChange',
87
+ 'keyPress',
88
+ 'change',
89
+ 'changeText',
90
+ 'selectionChange',
91
+ 'keyPress',
92
+ 'change',
93
+ 'changeText',
94
+ 'selectionChange',
95
+ 'endEditing',
96
+ 'blur',
97
+ ]);
98
+ });
99
+
100
+ it('supports defaultValue prop', async () => {
101
+ const { events, ...queries } = renderTextInputWithToolkit({
102
+ defaultValue: 'xxx',
103
+ });
104
+
105
+ const user = userEvent.setup();
106
+ await user.type(queries.getByTestId('input'), 'ab');
107
+
108
+ const eventNames = events.map((e) => e.name);
109
+ expect(eventNames).toEqual([
110
+ 'pressIn',
111
+ 'focus',
112
+ 'pressOut',
113
+ 'keyPress',
114
+ 'change',
115
+ 'changeText',
116
+ 'selectionChange',
117
+ 'keyPress',
118
+ 'change',
119
+ 'changeText',
120
+ 'selectionChange',
121
+ 'endEditing',
122
+ 'blur',
123
+ ]);
124
+
125
+ expect(events).toMatchSnapshot('input: "ab", defaultValue: "xxx"');
126
+ });
127
+
128
+ it('does respect editable prop', async () => {
129
+ const { events, ...queries } = renderTextInputWithToolkit({
130
+ editable: false,
131
+ });
132
+
133
+ const user = userEvent.setup();
134
+ await user.type(queries.getByTestId('input'), 'ab');
135
+
136
+ const eventNames = events.map((e) => e.name);
137
+ expect(eventNames).toEqual([]);
138
+ });
139
+
140
+ it('supports backspace', async () => {
141
+ const { events, ...queries } = renderTextInputWithToolkit({
142
+ defaultValue: 'xxx',
143
+ });
144
+
145
+ const user = userEvent.setup();
146
+ await user.type(queries.getByTestId('input'), '{Backspace}a');
147
+
148
+ const eventNames = events.map((e) => e.name);
149
+ expect(eventNames).toEqual([
150
+ 'pressIn',
151
+ 'focus',
152
+ 'pressOut',
153
+ 'keyPress',
154
+ 'change',
155
+ 'changeText',
156
+ 'selectionChange',
157
+ 'keyPress',
158
+ 'change',
159
+ 'changeText',
160
+ 'selectionChange',
161
+ 'endEditing',
162
+ 'blur',
163
+ ]);
164
+
165
+ expect(events).toMatchSnapshot(
166
+ 'input: "{Backspace}a", defaultValue: "xxx"'
18
167
  );
168
+ });
19
169
 
20
- await user.type(screen.getByTestId('input'), 'Hello World!');
170
+ it('supports multiline', async () => {
171
+ const { events, ...queries } = renderTextInputWithToolkit({
172
+ multiline: true,
173
+ });
21
174
 
22
- const eventNames = events.map((event) => event.name);
23
- expect(eventNames).toEqual(['focus', 'changeText', 'blur']);
24
- expect(events).toMatchSnapshot();
175
+ const user = userEvent.setup();
176
+ await user.type(queries.getByTestId('input'), '{Enter}\n');
177
+
178
+ const eventNames = events.map((e) => e.name);
179
+ expect(eventNames).toEqual([
180
+ 'pressIn',
181
+ 'focus',
182
+ 'pressOut',
183
+ 'keyPress',
184
+ 'textInput',
185
+ 'change',
186
+ 'changeText',
187
+ 'selectionChange',
188
+ 'contentSizeChange',
189
+ 'keyPress',
190
+ 'textInput',
191
+ 'change',
192
+ 'changeText',
193
+ 'selectionChange',
194
+ 'contentSizeChange',
195
+ 'endEditing',
196
+ 'blur',
197
+ ]);
198
+
199
+ expect(events).toMatchSnapshot('input: "{Enter}\\n", multiline: true');
25
200
  });
26
201
 
27
- it('supports direct access', async () => {
202
+ test('skips press events when `skipPress: true`', async () => {
203
+ const { events, ...queries } = renderTextInputWithToolkit();
204
+
205
+ const user = userEvent.setup();
206
+ await user.type(queries.getByTestId('input'), 'a', {
207
+ skipPress: true,
208
+ });
209
+
210
+ const eventNames = events.map((e) => e.name);
211
+ expect(eventNames).not.toContainEqual('pressIn');
212
+ expect(eventNames).not.toContainEqual('pressOut');
213
+ expect(eventNames).toEqual([
214
+ 'focus',
215
+ 'keyPress',
216
+ 'change',
217
+ 'changeText',
218
+ 'selectionChange',
219
+ 'endEditing',
220
+ 'blur',
221
+ ]);
222
+ });
223
+
224
+ it('triggers submit event with `submitEditing: true`', async () => {
225
+ const { events, ...queries } = renderTextInputWithToolkit();
226
+
227
+ const user = userEvent.setup();
228
+ await user.type(queries.getByTestId('input'), 'a', {
229
+ submitEditing: true,
230
+ });
231
+
232
+ const eventNames = events.map((e) => e.name);
233
+ expect(eventNames).toEqual([
234
+ 'pressIn',
235
+ 'focus',
236
+ 'pressOut',
237
+ 'keyPress',
238
+ 'change',
239
+ 'changeText',
240
+ 'selectionChange',
241
+ 'submitEditing',
242
+ 'endEditing',
243
+ 'blur',
244
+ ]);
245
+
246
+ expect(events[7].name).toBe('submitEditing');
247
+ expect(events[7].payload).toEqual({
248
+ nativeEvent: { text: 'a', target: 0 },
249
+ });
250
+ });
251
+
252
+ it('works when not all events have handlers', async () => {
28
253
  const { events, logEvent } = createEventLogger();
29
254
  const screen = render(
30
255
  <TextInput
31
256
  testID="input"
32
257
  onChangeText={logEvent('changeText')}
33
- onFocus={logEvent('focus')}
34
- onBlur={logEvent('blur')}
258
+ onEndEditing={logEvent('endEditing')}
35
259
  />
36
260
  );
37
261
 
38
- await userEvent.type(screen.getByTestId('input'), 'Hello World!');
262
+ const user = userEvent.setup();
263
+ await user.type(screen.getByTestId('input'), 'abc');
39
264
 
40
- const eventNames = events.map((event) => event.name);
41
- expect(eventNames).toEqual(['focus', 'changeText', 'blur']);
265
+ const eventNames = events.map((e) => e.name);
266
+ expect(eventNames).toEqual([
267
+ 'changeText',
268
+ 'changeText',
269
+ 'changeText',
270
+ 'endEditing',
271
+ ]);
272
+
273
+ expect(events).toMatchSnapshot('input: "abc"');
42
274
  });
43
275
 
44
- it.each(['modern', 'legacy'])('works with fake %s timers', async (type) => {
45
- jest.useFakeTimers({ legacyFakeTimers: type === 'legacy' });
276
+ it('does NOT work on View', async () => {
277
+ const screen = render(<View testID="input" />);
278
+
279
+ const user = userEvent.setup();
280
+ await expect(
281
+ user.type(screen.getByTestId('input'), 'abc')
282
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
283
+ `"type() works only with host "TextInput" elements. Passed element has type "View"."`
284
+ );
285
+ });
286
+
287
+ // View that ignores props type checking
288
+ const AnyView = View as React.ComponentType<any>;
289
+
290
+ it('does NOT bubble up', async () => {
291
+ const parentHandler = jest.fn();
292
+ const screen = render(
293
+ <AnyView
294
+ onChangeText={parentHandler}
295
+ onChange={parentHandler}
296
+ onKeyPress={parentHandler}
297
+ onTextInput={parentHandler}
298
+ onFocus={parentHandler}
299
+ onBlur={parentHandler}
300
+ onEndEditing={parentHandler}
301
+ onPressIn={parentHandler}
302
+ onPressOut={parentHandler}
303
+ >
304
+ <TextInput testID="input" />
305
+ </AnyView>
306
+ );
46
307
 
47
- const { events, logEvent } = createEventLogger();
48
308
  const user = userEvent.setup();
309
+ await user.type(screen.getByTestId('input'), 'abc');
310
+ expect(parentHandler).not.toHaveBeenCalled();
311
+ });
312
+
313
+ it('supports direct access', async () => {
314
+ const { events, logEvent } = createEventLogger();
49
315
  const screen = render(
50
316
  <TextInput
51
317
  testID="input"
@@ -55,9 +321,15 @@ describe('user.type()', () => {
55
321
  />
56
322
  );
57
323
 
58
- await user.type(screen.getByTestId('input'), 'Hello World!');
324
+ await userEvent.type(screen.getByTestId('input'), 'abc');
59
325
 
60
326
  const eventNames = events.map((event) => event.name);
61
- expect(eventNames).toEqual(['focus', 'changeText', 'blur']);
327
+ expect(eventNames).toEqual([
328
+ 'focus',
329
+ 'changeText',
330
+ 'changeText',
331
+ 'changeText',
332
+ 'blur',
333
+ ]);
62
334
  });
63
335
  });
@@ -1 +1 @@
1
- export { type } from './type';
1
+ export { type, TypeOptions } from './type';
@@ -0,0 +1,41 @@
1
+ const knownKeys = new Set(['Enter', 'Backspace']);
2
+
3
+ export function parseKeys(text: string) {
4
+ const result = [];
5
+
6
+ let remainingText = text;
7
+ while (remainingText) {
8
+ const [token, rest] = getNextToken(remainingText);
9
+ if (token.length > 1 && !knownKeys.has(token)) {
10
+ throw new Error(`Unknown key "${token}" in "${text}"`);
11
+ }
12
+
13
+ result.push(token);
14
+ remainingText = rest;
15
+ }
16
+
17
+ return result;
18
+ }
19
+
20
+ function getNextToken(text: string): [string, string] {
21
+ // Detect `{{` => escaped `{`
22
+ if (text[0] === '{' && text[1] === '{') {
23
+ return ['{', text.slice(2)];
24
+ }
25
+
26
+ // Detect `{key}` => special key
27
+ if (text[0] === '{') {
28
+ const endIndex = text.indexOf('}');
29
+ if (endIndex === -1) {
30
+ throw new Error(`Invalid key sequence "${text}"`);
31
+ }
32
+
33
+ return [text.slice(1, endIndex), text.slice(endIndex + 1)];
34
+ }
35
+
36
+ if (text[0] === '\n') {
37
+ return ['Enter', text.slice(1)];
38
+ }
39
+
40
+ return [text[0], text.slice(1)];
41
+ }
@@ -1,20 +1,138 @@
1
1
  import { ReactTestInstance } from 'react-test-renderer';
2
- import { UserEventInstance } from '../setup';
3
- import { dispatchHostEvent, wait } from '../utils';
2
+ import { isHostTextInput } from '../../helpers/host-component-names';
4
3
  import { EventBuilder } from '../event-builder';
4
+ import { ErrorWithStack } from '../../helpers/errors';
5
+ import { isPointerEventEnabled } from '../../helpers/pointer-events';
6
+ import { UserEventConfig, UserEventInstance } from '../setup';
7
+ import { dispatchEvent, wait, getTextContentSize } from '../utils';
8
+
9
+ import { parseKeys } from './parseKeys';
10
+
11
+ export interface TypeOptions {
12
+ skipPress?: boolean;
13
+ submitEditing?: boolean;
14
+ }
5
15
 
6
16
  export async function type(
7
17
  this: UserEventInstance,
8
18
  element: ReactTestInstance,
9
- text: string
10
- ) {
11
- // TODO provide real implementation
12
- await wait(this.config);
13
- dispatchHostEvent(element, 'focus', EventBuilder.Common.focus());
19
+ text: string,
20
+ options?: TypeOptions
21
+ ): Promise<void> {
22
+ if (!isHostTextInput(element)) {
23
+ throw new ErrorWithStack(
24
+ `type() works only with host "TextInput" elements. Passed element has type "${element.type}".`,
25
+ type
26
+ );
27
+ }
14
28
 
15
- await wait(this.config);
16
- dispatchHostEvent(element, 'changeText', text);
29
+ // Skip events if the element is disabled
30
+ if (element.props.editable === false || !isPointerEventEnabled(element)) {
31
+ return;
32
+ }
33
+
34
+ const keys = parseKeys(text);
17
35
 
36
+ if (!options?.skipPress) {
37
+ dispatchEvent(element, 'pressIn', EventBuilder.Common.touch());
38
+ }
39
+
40
+ dispatchEvent(element, 'focus', EventBuilder.Common.focus());
41
+
42
+ if (!options?.skipPress) {
43
+ await wait(this.config);
44
+ dispatchEvent(element, 'pressOut', EventBuilder.Common.touch());
45
+ }
46
+
47
+ let currentText = element.props.value ?? element.props.defaultValue ?? '';
48
+ for (const key of keys) {
49
+ const previousText = element.props.value ?? currentText;
50
+ currentText = applyKey(previousText, key);
51
+
52
+ await emitTypingEvents(
53
+ this.config,
54
+ element,
55
+ key,
56
+ currentText,
57
+ previousText
58
+ );
59
+ }
60
+
61
+ const finalText = element.props.value ?? currentText;
18
62
  await wait(this.config);
19
- dispatchHostEvent(element, 'blur', EventBuilder.Common.blur());
63
+
64
+ if (options?.submitEditing) {
65
+ dispatchEvent(
66
+ element,
67
+ 'submitEditing',
68
+ EventBuilder.TextInput.submitEditing(finalText)
69
+ );
70
+ }
71
+
72
+ dispatchEvent(
73
+ element,
74
+ 'endEditing',
75
+ EventBuilder.TextInput.endEditing(finalText)
76
+ );
77
+
78
+ dispatchEvent(element, 'blur', EventBuilder.Common.blur());
79
+ }
80
+
81
+ export async function emitTypingEvents(
82
+ config: UserEventConfig,
83
+ element: ReactTestInstance,
84
+ key: string,
85
+ currentText: string,
86
+ previousText: string
87
+ ) {
88
+ const isMultiline = element.props.multiline === true;
89
+
90
+ await wait(config);
91
+ dispatchEvent(element, 'keyPress', EventBuilder.TextInput.keyPress(key));
92
+
93
+ // According to the docs only multiline TextInput emits textInput event
94
+ // @see: https://github.com/facebook/react-native/blob/42a2898617da1d7a98ef574a5b9e500681c8f738/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts#L754
95
+ if (isMultiline) {
96
+ dispatchEvent(
97
+ element,
98
+ 'textInput',
99
+ EventBuilder.TextInput.textInput(currentText, previousText)
100
+ );
101
+ }
102
+
103
+ dispatchEvent(element, 'change', EventBuilder.TextInput.change(currentText));
104
+ dispatchEvent(element, 'changeText', currentText);
105
+
106
+ const selectionRange = {
107
+ start: currentText.length,
108
+ end: currentText.length,
109
+ };
110
+ dispatchEvent(
111
+ element,
112
+ 'selectionChange',
113
+ EventBuilder.TextInput.selectionChange(selectionRange)
114
+ );
115
+
116
+ // According to the docs only multiline TextInput emits contentSizeChange event
117
+ // @see: https://reactnative.dev/docs/textinput#oncontentsizechange
118
+ if (isMultiline) {
119
+ const contentSize = getTextContentSize(currentText);
120
+ dispatchEvent(
121
+ element,
122
+ 'contentSizeChange',
123
+ EventBuilder.TextInput.contentSizeChange(contentSize)
124
+ );
125
+ }
126
+ }
127
+
128
+ function applyKey(text: string, key: string) {
129
+ if (key === 'Enter') {
130
+ return `${text}\n`;
131
+ }
132
+
133
+ if (key === 'Backspace') {
134
+ return text.slice(0, -1);
135
+ }
136
+
137
+ return text + key;
20
138
  }