Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/event-builder/__tests__/text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import {
buildTextSelectionChangeEvent,
} from '../text';

test('buildTextChangeEvent returns event with text', () => {
const event = buildTextChangeEvent('Hello');
test('buildTextChangeEvent returns event with text and selection', () => {
const event = buildTextChangeEvent('Hello', { start: 5, end: 5 });

expect(event.nativeEvent).toEqual({
text: 'Hello',
target: 0,
eventCount: 0,
selection: { start: 5, end: 5 },
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/event-builder/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { baseSyntheticEvent } from './base';
* - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
* - Android: `{"eventCount": 6, "target": 53, "text": "Tes"}`
*/
export function buildTextChangeEvent(text: string) {
export function buildTextChangeEvent(text: string, { start, end }: TextRange) {
return {
...baseSyntheticEvent(),
nativeEvent: { text, target: 0, eventCount: 0 },
nativeEvent: { text, target: 0, eventCount: 0, selection: { start, end } },
};
}

Expand Down
12 changes: 12 additions & 0 deletions src/user-event/__tests__/__snapshots__/clear.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ exports[`clear() supports basic case: value: "Hello! 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 0,
"start": 0,
},
"target": 0,
"text": "",
},
Expand Down Expand Up @@ -202,6 +206,10 @@ exports[`clear() supports defaultValue prop: defaultValue: "Hello Default!" 1`]
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 0,
"start": 0,
},
"target": 0,
"text": "",
},
Expand Down Expand Up @@ -339,6 +347,10 @@ exports[`clear() supports multiline: value: "Hello World!\\nHow are you?" multil
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 0,
"start": 0,
},
"target": 0,
"text": "",
},
Expand Down
16 changes: 16 additions & 0 deletions src/user-event/__tests__/__snapshots__/paste.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ exports[`paste() paste on empty text input 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 3,
"start": 3,
},
"target": 0,
"text": "Hi!",
},
Expand Down Expand Up @@ -168,6 +172,10 @@ exports[`paste() paste on filled text input 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 3,
"start": 3,
},
"target": 0,
"text": "Hi!",
},
Expand Down Expand Up @@ -288,6 +296,10 @@ exports[`paste() supports defaultValue prop: defaultValue: "Hello Default!" 1`]
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 3,
"start": 3,
},
"target": 0,
"text": "Hi!",
},
Expand Down Expand Up @@ -408,6 +420,10 @@ exports[`paste() supports multiline: value: "Hello World!\\nHow are you?" multil
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 3,
"start": 3,
},
"target": 0,
"text": "Hi!",
},
Expand Down
4 changes: 2 additions & 2 deletions src/user-event/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ export async function paste(

// 3. Paste the text
nativeState.valueForInstance.set(instance, text);
await dispatchEvent(instance, 'change', buildTextChangeEvent(text));
await dispatchEvent(instance, 'changeText', text);

const rangeAfter = { start: text.length, end: text.length };
await dispatchEvent(instance, 'change', buildTextChangeEvent(text, rangeAfter));
await dispatchEvent(instance, 'changeText', text);
await dispatchEvent(instance, 'selectionChange', buildTextSelectionChangeEvent(rangeAfter));

// According to the docs only multiline TextInput emits contentSizeChange event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ exports[`type() for managed TextInput supports basic case: input: "Wow" 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 1,
"start": 1,
},
"target": 0,
"text": "W",
},
Expand Down Expand Up @@ -159,6 +163,10 @@ exports[`type() for managed TextInput supports basic case: input: "Wow" 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 2,
"start": 2,
},
"target": 0,
"text": "Wo",
},
Expand Down Expand Up @@ -219,6 +227,10 @@ exports[`type() for managed TextInput supports basic case: input: "Wow" 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 3,
"start": 3,
},
"target": 0,
"text": "Wow",
},
Expand Down Expand Up @@ -390,6 +402,10 @@ exports[`type() for managed TextInput supports rejecting TextInput: input: "ABC"
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 4,
"start": 4,
},
"target": 0,
"text": "XXXA",
},
Expand Down Expand Up @@ -450,6 +466,10 @@ exports[`type() for managed TextInput supports rejecting TextInput: input: "ABC"
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 4,
"start": 4,
},
"target": 0,
"text": "XXXB",
},
Expand Down Expand Up @@ -510,6 +530,10 @@ exports[`type() for managed TextInput supports rejecting TextInput: input: "ABC"
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 4,
"start": 4,
},
"target": 0,
"text": "XXXC",
},
Expand Down
36 changes: 36 additions & 0 deletions src/user-event/type/__tests__/__snapshots__/type.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ exports[`type() supports backspace: input: "{Backspace}a", defaultValue: "xxx" 1
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 2,
"start": 2,
},
"target": 0,
"text": "xx",
},
Expand Down Expand Up @@ -159,6 +163,10 @@ exports[`type() supports backspace: input: "{Backspace}a", defaultValue: "xxx" 1
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 3,
"start": 3,
},
"target": 0,
"text": "xxa",
},
Expand Down Expand Up @@ -330,6 +338,10 @@ exports[`type() supports basic case: input: "abc" 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 1,
"start": 1,
},
"target": 0,
"text": "a",
},
Expand Down Expand Up @@ -390,6 +402,10 @@ exports[`type() supports basic case: input: "abc" 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 2,
"start": 2,
},
"target": 0,
"text": "ab",
},
Expand Down Expand Up @@ -450,6 +466,10 @@ exports[`type() supports basic case: input: "abc" 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 3,
"start": 3,
},
"target": 0,
"text": "abc",
},
Expand Down Expand Up @@ -621,6 +641,10 @@ exports[`type() supports defaultValue prop: input: "ab", defaultValue: "xxx" 1`]
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 4,
"start": 4,
},
"target": 0,
"text": "xxxa",
},
Expand Down Expand Up @@ -681,6 +705,10 @@ exports[`type() supports defaultValue prop: input: "ab", defaultValue: "xxx" 1`]
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 5,
"start": 5,
},
"target": 0,
"text": "xxxab",
},
Expand Down Expand Up @@ -852,6 +880,10 @@ exports[`type() supports multiline: input: "{Enter}\\n", multiline: true 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 1,
"start": 1,
},
"target": 0,
"text": "
",
Expand Down Expand Up @@ -935,6 +967,10 @@ exports[`type() supports multiline: input: "{Enter}\\n", multiline: true 1`] = `
"isPropagationStopped": [Function],
"nativeEvent": {
"eventCount": 0,
"selection": {
"end": 2,
"start": 2,
},
"target": 0,
"text": "

Expand Down
50 changes: 50 additions & 0 deletions src/user-event/type/__tests__/type.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,56 @@ describe('type()', () => {
expect(events).toMatchSnapshot('input: "abc"');
});

it('includes caret selection in the change event nativeEvent', async () => {
const { events } = await renderTextInputWithToolkit();

const user = userEvent.setup();
await user.type(screen.getByTestId('input'), 'Hello');

const changeEvents = events.filter((event) => event.name === 'change');
const lastChangeEvent = changeEvents[changeEvents.length - 1];

// React Native (>=0.85) reports the caret position via `onChange`'s
// `nativeEvent.selection` on all platforms.
expect(lastChangeEvent.payload.nativeEvent.selection).toEqual({
start: 'Hello'.length,
end: 'Hello'.length,
});
});

it('drives components that read the caret from change event (regression)', async () => {
const onChange = jest.fn();

function MaskedInput() {
const [value, setValue] = React.useState('');
return (
<TextInput
testID="masked-input"
value={value}
onChange={(event) => {
// RN (>=0.85) reports the caret position via `nativeEvent.selection`
// on all platforms; the bundled RN types may not declare it yet.
const { selection, text } = event.nativeEvent as typeof event.nativeEvent & {
selection: { start: number; end: number };
};
onChange(selection);
// Mimic an input-mask library that relies on the caret position
// reported inside the `change` event to update the value.
setValue(text.slice(0, selection.end));
}}
/>
);
}

await render(<MaskedInput />);

const user = userEvent.setup();
await user.type(screen.getByTestId('masked-input'), 'abc');

expect(onChange).toHaveBeenLastCalledWith({ start: 3, end: 3 });
expect(screen.getByTestId('masked-input').props.value).toBe('abc');
});

it.each(['modern', 'legacy'])('works with %s fake timers', async (type) => {
jest.useFakeTimers({ legacyFakeTimers: type === 'legacy' });
const { events } = await renderTextInputWithToolkit();
Expand Down
5 changes: 3 additions & 2 deletions src/user-event/type/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,14 @@ export async function emitTypingEvents(
}

nativeState.valueForInstance.set(instance, text);
await dispatchEvent(instance, 'change', buildTextChangeEvent(text));
await dispatchEvent(instance, 'changeText', text);

const selectionRange = {
start: text.length,
end: text.length,
};

await dispatchEvent(instance, 'change', buildTextChangeEvent(text, selectionRange));
await dispatchEvent(instance, 'changeText', text);
await dispatchEvent(instance, 'selectionChange', buildTextSelectionChangeEvent(selectionRange));

// According to the docs only multiline TextInput emits contentSizeChange event
Expand Down