Skip to content

Commit 86e866e

Browse files
committed
refactor: code review changes
1 parent 5fbbbad commit 86e866e

File tree

3 files changed

+85
-51
lines changed

3 files changed

+85
-51
lines changed

src/user-event/clear.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,15 @@ export async function clear(this: UserEventInstance, element: ReactTestInstance)
3131
};
3232
dispatchEvent(element, 'selectionChange', EventBuilder.TextInput.selectionChange(selectionRange));
3333

34-
// 3. Press backspace
34+
// 3. Press backspace with selected text
3535
const finalText = '';
36-
await emitTypingEvents(this.config, element, 'Backspace', finalText, previousText);
36+
await emitTypingEvents(element, {
37+
config: this.config,
38+
key: 'Backspace',
39+
text: finalText,
40+
previousText,
41+
isAccepted: true,
42+
});
3743

3844
// 4. Exit element
3945
await wait(this.config);

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

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { TextInput, TextInputProps, View } from 'react-native';
3-
import { createEventLogger } from '../../../test-utils';
3+
import { createEventLogger, EventEntry } from '../../../test-utils';
44
import { render, screen } from '../../..';
55
import { userEvent } from '../..';
66

@@ -31,18 +31,17 @@ function renderTextInputWithToolkit(props: TextInputProps = {}) {
3131
);
3232

3333
return {
34-
...screen,
3534
events,
3635
};
3736
}
3837

3938
describe('type()', () => {
4039
it('supports basic case', async () => {
4140
jest.spyOn(Date, 'now').mockImplementation(() => 100100100100);
42-
const { events, ...queries } = renderTextInputWithToolkit();
41+
const { events } = renderTextInputWithToolkit();
4342

4443
const user = userEvent.setup();
45-
await user.type(queries.getByTestId('input'), 'abc');
44+
await user.type(screen.getByTestId('input'), 'abc');
4645

4746
const eventNames = events.map((e) => e.name);
4847
expect(eventNames).toEqual([
@@ -70,10 +69,10 @@ describe('type()', () => {
7069

7170
it.each(['modern', 'legacy'])('works with %s fake timers', async (type) => {
7271
jest.useFakeTimers({ legacyFakeTimers: type === 'legacy' });
73-
const { events, ...queries } = renderTextInputWithToolkit();
72+
const { events } = renderTextInputWithToolkit();
7473

7574
const user = userEvent.setup();
76-
await user.type(queries.getByTestId('input'), 'abc');
75+
await user.type(screen.getByTestId('input'), 'abc');
7776

7877
const eventNames = events.map((e) => e.name);
7978
expect(eventNames).toEqual([
@@ -98,12 +97,12 @@ describe('type()', () => {
9897
});
9998

10099
it('supports defaultValue prop', async () => {
101-
const { events, ...queries } = renderTextInputWithToolkit({
100+
const { events } = renderTextInputWithToolkit({
102101
defaultValue: 'xxx',
103102
});
104103

105104
const user = userEvent.setup();
106-
await user.type(queries.getByTestId('input'), 'ab');
105+
await user.type(screen.getByTestId('input'), 'ab');
107106

108107
const eventNames = events.map((e) => e.name);
109108
expect(eventNames).toEqual([
@@ -126,24 +125,24 @@ describe('type()', () => {
126125
});
127126

128127
it('does respect editable prop', async () => {
129-
const { events, ...queries } = renderTextInputWithToolkit({
128+
const { events } = renderTextInputWithToolkit({
130129
editable: false,
131130
});
132131

133132
const user = userEvent.setup();
134-
await user.type(queries.getByTestId('input'), 'ab');
133+
await user.type(screen.getByTestId('input'), 'ab');
135134

136135
const eventNames = events.map((e) => e.name);
137136
expect(eventNames).toEqual([]);
138137
});
139138

140139
it('supports backspace', async () => {
141-
const { events, ...queries } = renderTextInputWithToolkit({
140+
const { events } = renderTextInputWithToolkit({
142141
defaultValue: 'xxx',
143142
});
144143

145144
const user = userEvent.setup();
146-
await user.type(queries.getByTestId('input'), '{Backspace}a');
145+
await user.type(screen.getByTestId('input'), '{Backspace}a');
147146

148147
const eventNames = events.map((e) => e.name);
149148
expect(eventNames).toEqual([
@@ -166,12 +165,12 @@ describe('type()', () => {
166165
});
167166

168167
it('supports multiline', async () => {
169-
const { events, ...queries } = renderTextInputWithToolkit({
168+
const { events } = renderTextInputWithToolkit({
170169
multiline: true,
171170
});
172171

173172
const user = userEvent.setup();
174-
await user.type(queries.getByTestId('input'), '{Enter}\n');
173+
await user.type(screen.getByTestId('input'), '{Enter}\n');
175174

176175
const eventNames = events.map((e) => e.name);
177176
expect(eventNames).toEqual([
@@ -198,10 +197,10 @@ describe('type()', () => {
198197
});
199198

200199
test('skips press events when `skipPress: true`', async () => {
201-
const { events, ...queries } = renderTextInputWithToolkit();
200+
const { events } = renderTextInputWithToolkit();
202201

203202
const user = userEvent.setup();
204-
await user.type(queries.getByTestId('input'), 'a', {
203+
await user.type(screen.getByTestId('input'), 'a', {
205204
skipPress: true,
206205
});
207206

@@ -217,13 +216,17 @@ describe('type()', () => {
217216
'endEditing',
218217
'blur',
219218
]);
219+
220+
expect(lastEvent(events, 'endEditing')?.payload).toMatchObject({
221+
nativeEvent: { text: 'a', target: 0 },
222+
});
220223
});
221224

222225
it('triggers submit event with `submitEditing: true`', async () => {
223-
const { events, ...queries } = renderTextInputWithToolkit();
226+
const { events } = renderTextInputWithToolkit();
224227

225228
const user = userEvent.setup();
226-
await user.type(queries.getByTestId('input'), 'a', {
229+
await user.type(screen.getByTestId('input'), 'a', {
227230
submitEditing: true,
228231
});
229232

@@ -241,8 +244,7 @@ describe('type()', () => {
241244
'blur',
242245
]);
243246

244-
expect(events[7].name).toBe('submitEditing');
245-
expect(events[7].payload).toMatchObject({
247+
expect(lastEvent(events, 'submitEditing')?.payload).toMatchObject({
246248
nativeEvent: { text: 'a', target: 0 },
247249
currentTarget: {},
248250
target: {},
@@ -339,35 +341,39 @@ describe('type()', () => {
339341
expect(handleKeyPress).toHaveBeenCalledTimes(3);
340342
});
341343

342-
it('does respect maxLength prop', async () => {
343-
const { events, ...queries } = renderTextInputWithToolkit({
344-
maxLength: 2,
345-
});
344+
it('respects the "maxLength" prop', async () => {
345+
const { events } = renderTextInputWithToolkit({ maxLength: 2 });
346346

347347
const user = userEvent.setup();
348-
await user.type(queries.getByTestId('input'), 'abc');
348+
await user.type(screen.getByTestId('input'), 'abcd');
349349

350350
const eventNames = events.map((e) => e.name);
351351
expect(eventNames).toEqual([
352352
'pressIn',
353353
'focus',
354354
'pressOut',
355-
'keyPress',
355+
'keyPress', // a
356356
'change',
357357
'changeText',
358358
'selectionChange',
359-
'keyPress',
359+
'keyPress', // b
360360
'change',
361361
'changeText',
362362
'selectionChange',
363+
'keyPress', // c
364+
'keyPress', // d
363365
'endEditing',
364366
'blur',
365367
]);
366368

367-
const lastChangeTestEvent = events.filter((e) => e.name === 'changeText').pop();
368-
expect(lastChangeTestEvent).toMatchObject({
369-
name: 'changeText',
370-
payload: 'ab',
369+
expect(lastEvent(events, 'changeText')?.payload).toBe('ab');
370+
expect(lastEvent(events, 'endEditing')?.payload.nativeEvent).toMatchObject({
371+
target: 0,
372+
text: 'ab',
371373
});
372374
});
373375
});
376+
377+
function lastEvent(events: EventEntry[], name: string) {
378+
return events.filter((e) => e.name === name).pop();
379+
}

src/user-event/type/type.ts

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,20 @@ export async function type(
4747
let currentText = element.props.value ?? element.props.defaultValue ?? '';
4848
for (const key of keys) {
4949
const previousText = element.props.value ?? currentText;
50-
currentText = applyKey(previousText, key);
51-
52-
if (element.props.maxLength === undefined || currentText.length <= element.props.maxLength) {
53-
await emitTypingEvents(this.config, element, key, currentText, previousText);
50+
const proposedText = applyKey(previousText, key);
51+
let isAccepted = false;
52+
if (isTextChangeAllowed(element, proposedText)) {
53+
currentText = proposedText;
54+
isAccepted = true;
5455
}
56+
57+
await emitTypingEvents(element, {
58+
config: this.config,
59+
key,
60+
text: currentText,
61+
previousText,
62+
isAccepted,
63+
});
5564
}
5665

5766
const finalText = element.props.value ?? currentText;
@@ -66,41 +75,49 @@ export async function type(
6675
dispatchEvent(element, 'blur', EventBuilder.Common.blur());
6776
}
6877

78+
type EmitTypingEventsContext = {
79+
config: UserEventConfig;
80+
key: string;
81+
text: string;
82+
previousText: string;
83+
isAccepted?: boolean;
84+
};
85+
6986
export async function emitTypingEvents(
70-
config: UserEventConfig,
7187
element: ReactTestInstance,
72-
key: string,
73-
currentText: string,
74-
previousText: string,
88+
{ config, key, text, previousText, isAccepted }: EmitTypingEventsContext,
7589
) {
7690
const isMultiline = element.props.multiline === true;
7791

7892
await wait(config);
7993
dispatchEvent(element, 'keyPress', EventBuilder.TextInput.keyPress(key));
8094

95+
// Platform difference (based on experiments):
96+
// - iOS and RN Web: TextInput emits only `keyPress` event when max length has been reached
97+
// - Android: TextInputs does not emit any events
98+
if (isAccepted === false) {
99+
return;
100+
}
101+
81102
// According to the docs only multiline TextInput emits textInput event
82103
// @see: https://github.com/facebook/react-native/blob/42a2898617da1d7a98ef574a5b9e500681c8f738/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts#L754
83104
if (isMultiline) {
84-
dispatchEvent(
85-
element,
86-
'textInput',
87-
EventBuilder.TextInput.textInput(currentText, previousText),
88-
);
105+
dispatchEvent(element, 'textInput', EventBuilder.TextInput.textInput(text, previousText));
89106
}
90107

91-
dispatchEvent(element, 'change', EventBuilder.TextInput.change(currentText));
92-
dispatchEvent(element, 'changeText', currentText);
108+
dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
109+
dispatchEvent(element, 'changeText', text);
93110

94111
const selectionRange = {
95-
start: currentText.length,
96-
end: currentText.length,
112+
start: text.length,
113+
end: text.length,
97114
};
98115
dispatchEvent(element, 'selectionChange', EventBuilder.TextInput.selectionChange(selectionRange));
99116

100117
// According to the docs only multiline TextInput emits contentSizeChange event
101118
// @see: https://reactnative.dev/docs/textinput#oncontentsizechange
102119
if (isMultiline) {
103-
const contentSize = getTextContentSize(currentText);
120+
const contentSize = getTextContentSize(text);
104121
dispatchEvent(
105122
element,
106123
'contentSizeChange',
@@ -120,3 +137,8 @@ function applyKey(text: string, key: string) {
120137

121138
return text + key;
122139
}
140+
141+
function isTextChangeAllowed(element: ReactTestInstance, text: string) {
142+
const maxLength = element.props.maxLength;
143+
return maxLength === undefined || text.length <= maxLength;
144+
}

0 commit comments

Comments
 (0)