From ea9fc0ec1c0065468b64dccee82dcae177b7b819 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= <mdjastrzebski@gmail.com>
Date: Tue, 13 Aug 2024 12:15:56 +0200
Subject: [PATCH 1/3] feat: basic paste() implementation

---
 src/user-event/index.ts       |  1 +
 src/user-event/paste.ts       | 57 +++++++++++++++++++++++++++++++++++
 src/user-event/setup/setup.ts |  2 ++
 3 files changed, 60 insertions(+)
 create mode 100644 src/user-event/paste.ts

diff --git a/src/user-event/index.ts b/src/user-event/index.ts
index 6d8e50b63..17e0c946f 100644
--- a/src/user-event/index.ts
+++ b/src/user-event/index.ts
@@ -16,6 +16,7 @@ export const userEvent = {
   type: (element: ReactTestInstance, text: string, options?: TypeOptions) =>
     setup().type(element, text, options),
   clear: (element: ReactTestInstance) => setup().clear(element),
+  paste: (element: ReactTestInstance, text: string) => setup().paste(element, text),
   scrollTo: (element: ReactTestInstance, options: ScrollToOptions) =>
     setup().scrollTo(element, options),
 };
diff --git a/src/user-event/paste.ts b/src/user-event/paste.ts
new file mode 100644
index 000000000..80ed6b2bd
--- /dev/null
+++ b/src/user-event/paste.ts
@@ -0,0 +1,57 @@
+import { ReactTestInstance } from 'react-test-renderer';
+import { ErrorWithStack } from '../helpers/errors';
+import { isHostTextInput } from '../helpers/host-component-names';
+import { isPointerEventEnabled } from '../helpers/pointer-events';
+import { isTextInputEditable } from '../helpers/text-input';
+import { EventBuilder } from './event-builder';
+import { UserEventInstance } from './setup';
+import { dispatchEvent, getTextContentSize, wait } from './utils';
+
+export async function paste(
+  this: UserEventInstance,
+  element: ReactTestInstance,
+  text: string,
+): Promise<void> {
+  if (!isHostTextInput(element)) {
+    throw new ErrorWithStack(
+      `paste() only supports host "TextInput" elements. Passed element has type: "${element.type}".`,
+      paste,
+    );
+  }
+
+  if (!isTextInputEditable(element) || !isPointerEventEnabled(element)) {
+    return;
+  }
+
+  // 1. Enter element
+  dispatchEvent(element, 'focus', EventBuilder.Common.focus());
+
+  // 2. Select all
+  const textToClear = element.props.value ?? element.props.defaultValue ?? '';
+  const rangeToClear = { start: 0, end: textToClear.length };
+  dispatchEvent(element, 'selectionChange', EventBuilder.TextInput.selectionChange(rangeToClear));
+
+  // 3. Paste the text
+  dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
+  dispatchEvent(element, 'changeText', text);
+
+  const rangeAfter = { start: text.length, end: text.length };
+  dispatchEvent(element, 'selectionChange', EventBuilder.TextInput.selectionChange(rangeAfter));
+
+  // According to the docs only multiline TextInput emits contentSizeChange event
+  // @see: https://reactnative.dev/docs/textinput#oncontentsizechange
+  const isMultiline = element.props.multiline === true;
+  if (isMultiline) {
+    const contentSize = getTextContentSize(text);
+    dispatchEvent(
+      element,
+      'contentSizeChange',
+      EventBuilder.TextInput.contentSizeChange(contentSize),
+    );
+  }
+
+  // 4. Exit element
+  await wait(this.config);
+  dispatchEvent(element, 'endEditing', EventBuilder.TextInput.endEditing(text));
+  dispatchEvent(element, 'blur', EventBuilder.Common.blur());
+}
diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts
index 5091793f1..b0772d17a 100644
--- a/src/user-event/setup/setup.ts
+++ b/src/user-event/setup/setup.ts
@@ -2,6 +2,7 @@ import { ReactTestInstance } from 'react-test-renderer';
 import { jestFakeTimersAreEnabled } from '../../helpers/timers';
 import { wrapAsync } from '../../helpers/wrap-async';
 import { clear } from '../clear';
+import { paste } from '../paste';
 import { PressOptions, press, longPress } from '../press';
 import { ScrollToOptions, scrollTo } from '../scroll';
 import { TypeOptions, type } from '../type';
@@ -139,6 +140,7 @@ function createInstance(config: UserEventConfig): UserEventInstance {
     longPress: wrapAndBindImpl(instance, longPress),
     type: wrapAndBindImpl(instance, type),
     clear: wrapAndBindImpl(instance, clear),
+    paste: wrapAndBindImpl(instance, paste),
     scrollTo: wrapAndBindImpl(instance, scrollTo),
   };
 

From d66588aba9c4f1915c7877f34d0d580e9f01ad37 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= <mdjastrzebski@gmail.com>
Date: Tue, 13 Aug 2024 16:29:14 +0200
Subject: [PATCH 2/3] feat: basic tests

---
 .../__snapshots__/paste.test.tsx.snap         | 530 ++++++++++++++++++
 src/user-event/__tests__/paste.test.tsx       | 224 ++++++++
 src/user-event/setup/setup.ts                 |  13 +
 3 files changed, 767 insertions(+)
 create mode 100644 src/user-event/__tests__/__snapshots__/paste.test.tsx.snap
 create mode 100644 src/user-event/__tests__/paste.test.tsx

diff --git a/src/user-event/__tests__/__snapshots__/paste.test.tsx.snap b/src/user-event/__tests__/__snapshots__/paste.test.tsx.snap
new file mode 100644
index 000000000..fe5a510ca
--- /dev/null
+++ b/src/user-event/__tests__/__snapshots__/paste.test.tsx.snap
@@ -0,0 +1,530 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`paste() paste on empty text input 1`] = `
+[
+  {
+    "name": "focus",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "selectionChange",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "selection": {
+          "end": 0,
+          "start": 0,
+        },
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "change",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "eventCount": 0,
+        "target": 0,
+        "text": "Hi!",
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "changeText",
+    "payload": "Hi!",
+  },
+  {
+    "name": "selectionChange",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "selection": {
+          "end": 3,
+          "start": 3,
+        },
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "endEditing",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+        "text": "Hi!",
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "blur",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+]
+`;
+
+exports[`paste() paste on filled text input 1`] = `
+[
+  {
+    "name": "focus",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "selectionChange",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "selection": {
+          "end": 6,
+          "start": 0,
+        },
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "change",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "eventCount": 0,
+        "target": 0,
+        "text": "Hi!",
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "changeText",
+    "payload": "Hi!",
+  },
+  {
+    "name": "selectionChange",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "selection": {
+          "end": 3,
+          "start": 3,
+        },
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "endEditing",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+        "text": "Hi!",
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "blur",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+]
+`;
+
+exports[`paste() supports defaultValue prop: defaultValue: "Hello Default!" 1`] = `
+[
+  {
+    "name": "focus",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "selectionChange",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "selection": {
+          "end": 14,
+          "start": 0,
+        },
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "change",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "eventCount": 0,
+        "target": 0,
+        "text": "Hi!",
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "changeText",
+    "payload": "Hi!",
+  },
+  {
+    "name": "selectionChange",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "selection": {
+          "end": 3,
+          "start": 3,
+        },
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "endEditing",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+        "text": "Hi!",
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "blur",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+]
+`;
+
+exports[`paste() supports multiline: value: "Hello World!
+How are you?" multiline: true, 1`] = `
+[
+  {
+    "name": "focus",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "selectionChange",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "selection": {
+          "end": 25,
+          "start": 0,
+        },
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "change",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "eventCount": 0,
+        "target": 0,
+        "text": "Hi!",
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "changeText",
+    "payload": "Hi!",
+  },
+  {
+    "name": "selectionChange",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "selection": {
+          "end": 3,
+          "start": 3,
+        },
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "contentSizeChange",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "contentSize": {
+          "height": 16,
+          "width": 15,
+        },
+        "target": 0,
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "endEditing",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+        "text": "Hi!",
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+  {
+    "name": "blur",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+]
+`;
+
+exports[`paste() works when not all events have handlers 1`] = `
+[
+  {
+    "name": "changeText",
+    "payload": "Hi!",
+  },
+  {
+    "name": "endEditing",
+    "payload": {
+      "currentTarget": {},
+      "isDefaultPrevented": [Function],
+      "isPersistent": [Function],
+      "isPropagationStopped": [Function],
+      "nativeEvent": {
+        "target": 0,
+        "text": "Hi!",
+      },
+      "persist": [Function],
+      "preventDefault": [Function],
+      "stopPropagation": [Function],
+      "target": {},
+      "timeStamp": 0,
+    },
+  },
+]
+`;
diff --git a/src/user-event/__tests__/paste.test.tsx b/src/user-event/__tests__/paste.test.tsx
new file mode 100644
index 000000000..1c01c8b0b
--- /dev/null
+++ b/src/user-event/__tests__/paste.test.tsx
@@ -0,0 +1,224 @@
+import * as React from 'react';
+import { TextInput, TextInputProps, View } from 'react-native';
+import { createEventLogger, getEventsNames } from '../../test-utils';
+import { render, userEvent, screen } from '../..';
+
+beforeEach(() => {
+  jest.useRealTimers();
+});
+
+function renderTextInputWithToolkit(props: TextInputProps = {}) {
+  const { events, logEvent } = createEventLogger();
+
+  render(
+    <TextInput
+      testID="input"
+      onFocus={logEvent('focus')}
+      onBlur={logEvent('blur')}
+      onPressIn={logEvent('pressIn')}
+      onPressOut={logEvent('pressOut')}
+      onChange={logEvent('change')}
+      onChangeText={logEvent('changeText')}
+      onKeyPress={logEvent('keyPress')}
+      onSelectionChange={logEvent('selectionChange')}
+      onSubmitEditing={logEvent('submitEditing')}
+      onEndEditing={logEvent('endEditing')}
+      onContentSizeChange={logEvent('contentSizeChange')}
+      {...props}
+    />,
+  );
+
+  const textInput = screen.getByTestId('input');
+
+  return {
+    events,
+    textInput,
+  };
+}
+
+describe('paste()', () => {
+  it('paste on empty text input', async () => {
+    jest.spyOn(Date, 'now').mockImplementation(() => 100100100100);
+    const { textInput, events } = renderTextInputWithToolkit();
+
+    const user = userEvent.setup();
+    await user.paste(textInput, 'Hi!');
+
+    expect(getEventsNames(events)).toEqual([
+      'focus',
+      'selectionChange',
+      'change',
+      'changeText',
+      'selectionChange',
+      'endEditing',
+      'blur',
+    ]);
+
+    expect(events).toMatchSnapshot();
+  });
+
+  it('paste on filled text input', async () => {
+    jest.spyOn(Date, 'now').mockImplementation(() => 100100100100);
+    const { textInput, events } = renderTextInputWithToolkit({
+      value: 'Hello!',
+    });
+
+    const user = userEvent.setup();
+    await user.paste(textInput, 'Hi!');
+
+    expect(getEventsNames(events)).toEqual([
+      'focus',
+      'selectionChange',
+      'change',
+      'changeText',
+      'selectionChange',
+      'endEditing',
+      'blur',
+    ]);
+
+    expect(events).toMatchSnapshot();
+  });
+
+  it.each(['modern', 'legacy'])('works with %s fake timers', async (type) => {
+    jest.useFakeTimers({ legacyFakeTimers: type === 'legacy' });
+    const { textInput, events } = renderTextInputWithToolkit({
+      value: 'Hello!',
+    });
+
+    const user = userEvent.setup();
+    await user.paste(textInput, 'Hi!');
+
+    expect(getEventsNames(events)).toEqual([
+      'focus',
+      'selectionChange',
+      'change',
+      'changeText',
+      'selectionChange',
+      'endEditing',
+      'blur',
+    ]);
+  });
+
+  it('supports defaultValue prop', async () => {
+    const { textInput, events } = renderTextInputWithToolkit({
+      defaultValue: 'Hello Default!',
+    });
+
+    const user = userEvent.setup();
+    await user.paste(textInput, 'Hi!');
+
+    expect(getEventsNames(events)).toEqual([
+      'focus',
+      'selectionChange',
+      'change',
+      'changeText',
+      'selectionChange',
+      'endEditing',
+      'blur',
+    ]);
+
+    expect(events).toMatchSnapshot('defaultValue: "Hello Default!"');
+  });
+
+  it('does respect editable prop', async () => {
+    const { textInput } = renderTextInputWithToolkit({
+      value: 'Hello!',
+      editable: false,
+    });
+
+    const user = userEvent.setup();
+    await user.paste(textInput, 'Hi!');
+
+    expect(textInput.props.value).toBe('Hello!');
+  });
+
+  it('does respect pointer-events prop', async () => {
+    const { textInput } = renderTextInputWithToolkit({
+      value: 'Hello!',
+      pointerEvents: 'none',
+    });
+
+    const user = userEvent.setup();
+    await user.paste(textInput, 'Hi!');
+
+    expect(textInput.props.value).toBe('Hello!');
+  });
+
+  it('supports multiline', async () => {
+    const { textInput, events } = renderTextInputWithToolkit({
+      value: 'Hello World!\nHow are you?',
+      multiline: true,
+    });
+
+    const user = userEvent.setup();
+    await user.paste(textInput, 'Hi!');
+
+    expect(getEventsNames(events)).toEqual([
+      'focus',
+      'selectionChange',
+      'change',
+      'changeText',
+      'selectionChange',
+      'contentSizeChange',
+      'endEditing',
+      'blur',
+    ]);
+
+    expect(events).toMatchSnapshot('value: "Hello World!\nHow are you?" multiline: true,');
+  });
+
+  it('works when not all events have handlers', async () => {
+    const { events, logEvent } = createEventLogger();
+    render(
+      <TextInput
+        testID="input"
+        onChangeText={logEvent('changeText')}
+        onEndEditing={logEvent('endEditing')}
+      />,
+    );
+
+    const user = userEvent.setup();
+    await user.paste(screen.getByTestId('input'), 'Hi!');
+
+    expect(getEventsNames(events)).toEqual(['changeText', 'endEditing']);
+
+    expect(events).toMatchSnapshot();
+  });
+
+  it('does NOT work on View', async () => {
+    render(<View testID="input" />);
+
+    const user = userEvent.setup();
+    await expect(
+      user.paste(screen.getByTestId('input'), 'Hi!'),
+    ).rejects.toThrowErrorMatchingInlineSnapshot(
+      `"paste() only supports host "TextInput" elements. Passed element has type: "View"."`,
+    );
+  });
+
+  // View that ignores props type checking
+  const AnyView = View as React.ComponentType<any>;
+
+  it('does NOT bubble up', async () => {
+    const parentHandler = jest.fn();
+    render(
+      <AnyView
+        onChangeText={parentHandler}
+        onChange={parentHandler}
+        onKeyPress={parentHandler}
+        onTextInput={parentHandler}
+        onFocus={parentHandler}
+        onBlur={parentHandler}
+        onEndEditing={parentHandler}
+        onPressIn={parentHandler}
+        onPressOut={parentHandler}
+      >
+        <TextInput testID="input" />
+      </AnyView>,
+    );
+
+    const user = userEvent.setup();
+    await user.paste(screen.getByTestId('input'), 'Hi!');
+    expect(parentHandler).not.toHaveBeenCalled();
+  });
+});
diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts
index b0772d17a..53a4befa8 100644
--- a/src/user-event/setup/setup.ts
+++ b/src/user-event/setup/setup.ts
@@ -120,6 +120,19 @@ export interface UserEventInstance {
    */
   clear: (element: ReactTestInstance) => Promise<void>;
 
+  /**
+   * Simulate user pasting the text to a given `TextInput` element.
+   *
+   * This method will simulate:
+   * 1. entering TextInput
+   * 2. selecting all text
+   * 3. paste the text
+   * 4. leaving TextInput
+   *
+   * @param element TextInput element to paste to
+   */
+  paste: (element: ReactTestInstance, text: string) => Promise<void>;
+
   /**
    * Simlate user scorlling a ScrollView element.
    *

From add403b3e4451f6d8433dcb1357f2fc7ce9d05dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= <mdjastrzebski@gmail.com>
Date: Tue, 13 Aug 2024 20:35:59 +0200
Subject: [PATCH 3/3] docs: update docs

---
 .../docs/12.x/docs/api/events/user-event.mdx  | 52 ++++++++++++++++---
 1 file changed, 44 insertions(+), 8 deletions(-)

diff --git a/website/docs/12.x/docs/api/events/user-event.mdx b/website/docs/12.x/docs/api/events/user-event.mdx
index 50d657ff9..fbf46af74 100644
--- a/website/docs/12.x/docs/api/events/user-event.mdx
+++ b/website/docs/12.x/docs/api/events/user-event.mdx
@@ -124,12 +124,10 @@ The `pressIn` and `pressOut` events are sent by default but can be skipped by pa
 **Typing (for each character)**:
 
 - `keyPress`
-- `textInput` (optional)
 - `change`
 - `changeText`
 - `selectionChange`
-
-The `textInput` event is sent only for multiline text inputs.
+- `contentSizeChange` (only multiline)
 
 **Leaving the element**:
 
@@ -144,7 +142,7 @@ The `submitEditing` event is skipped by default. It can sent by setting the `sub
 ```ts
 clear(
   element: ReactTestInstance,
-}
+)
 ```
 
 Example
@@ -160,8 +158,6 @@ This function supports only host `TextInput` elements. Passing other element typ
 
 ### Sequence of events
 
-The sequence of events depends on the `multiline` prop and passed options.
-
 Events will not be emitted if the `editable` prop is set to `false`.
 
 **Entering the element**:
@@ -175,12 +171,52 @@ Events will not be emitted if the `editable` prop is set to `false`.
 **Pressing backspace**:
 
 - `keyPress`
-- `textInput` (optional)
 - `change`
 - `changeText`
 - `selectionChange`
 
-The `textInput` event is sent only for multiline text inputs.
+**Leaving the element**:
+
+- `endEditing`
+- `blur`
+
+## `paste()`
+
+```ts
+paste(
+  element: ReactTestInstance,
+  text: string,
+)
+```
+
+Example
+
+```ts
+const user = userEvent.setup();
+await user.paste(textInput, 'Text to paste');
+```
+
+This helper simulates the user pasting given text to a `TextInput` element.
+
+This function supports only host `TextInput` elements. Passing other element types will result in throwing an error.
+
+### Sequence of events
+
+Events will not be emitted if the `editable` prop is set to `false`.
+
+**Entering the element**:
+
+- `focus`
+
+**Selecting all content**:
+
+- `selectionChange`
+
+**Pasting the text**:
+
+- `change`
+- `changeText`
+- `selectionChange`
 
 **Leaving the element**: