diff --git a/src/user-event/__tests__/__snapshots__/clear.test.tsx.snap b/src/user-event/__tests__/__snapshots__/clear.test.tsx.snap
new file mode 100644
index 000000000..9d0c7d1a3
--- /dev/null
+++ b/src/user-event/__tests__/__snapshots__/clear.test.tsx.snap
@@ -0,0 +1,269 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`clear() supports basic case: value: "Hello! 1`] = `
+[
+ {
+ "name": "focus",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ },
+ },
+ },
+ {
+ "name": "selectionChange",
+ "payload": {
+ "nativeEvent": {
+ "selection": {
+ "end": 6,
+ "start": 0,
+ },
+ },
+ },
+ },
+ {
+ "name": "keyPress",
+ "payload": {
+ "nativeEvent": {
+ "key": "Backspace",
+ },
+ },
+ },
+ {
+ "name": "change",
+ "payload": {
+ "nativeEvent": {
+ "eventCount": 0,
+ "target": 0,
+ "text": "",
+ },
+ },
+ },
+ {
+ "name": "changeText",
+ "payload": "",
+ },
+ {
+ "name": "selectionChange",
+ "payload": {
+ "nativeEvent": {
+ "selection": {
+ "end": 0,
+ "start": 0,
+ },
+ },
+ },
+ },
+ {
+ "name": "endEditing",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ "text": "",
+ },
+ },
+ },
+ {
+ "name": "blur",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ },
+ },
+ },
+]
+`;
+
+exports[`clear() supports defaultValue prop: defaultValue: "Hello Default!" 1`] = `
+[
+ {
+ "name": "focus",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ },
+ },
+ },
+ {
+ "name": "selectionChange",
+ "payload": {
+ "nativeEvent": {
+ "selection": {
+ "end": 14,
+ "start": 0,
+ },
+ },
+ },
+ },
+ {
+ "name": "keyPress",
+ "payload": {
+ "nativeEvent": {
+ "key": "Backspace",
+ },
+ },
+ },
+ {
+ "name": "change",
+ "payload": {
+ "nativeEvent": {
+ "eventCount": 0,
+ "target": 0,
+ "text": "",
+ },
+ },
+ },
+ {
+ "name": "changeText",
+ "payload": "",
+ },
+ {
+ "name": "selectionChange",
+ "payload": {
+ "nativeEvent": {
+ "selection": {
+ "end": 0,
+ "start": 0,
+ },
+ },
+ },
+ },
+ {
+ "name": "endEditing",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ "text": "",
+ },
+ },
+ },
+ {
+ "name": "blur",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ },
+ },
+ },
+]
+`;
+
+exports[`clear() supports multiline: value: "Hello World!
+How are you?" multiline: true, 1`] = `
+[
+ {
+ "name": "focus",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ },
+ },
+ },
+ {
+ "name": "selectionChange",
+ "payload": {
+ "nativeEvent": {
+ "selection": {
+ "end": 25,
+ "start": 0,
+ },
+ },
+ },
+ },
+ {
+ "name": "keyPress",
+ "payload": {
+ "nativeEvent": {
+ "key": "Backspace",
+ },
+ },
+ },
+ {
+ "name": "textInput",
+ "payload": {
+ "nativeEvent": {
+ "previousText": "Hello World!
+How are you?",
+ "range": {
+ "end": 0,
+ "start": 0,
+ },
+ "target": 0,
+ "text": "",
+ },
+ },
+ },
+ {
+ "name": "change",
+ "payload": {
+ "nativeEvent": {
+ "eventCount": 0,
+ "target": 0,
+ "text": "",
+ },
+ },
+ },
+ {
+ "name": "changeText",
+ "payload": "",
+ },
+ {
+ "name": "selectionChange",
+ "payload": {
+ "nativeEvent": {
+ "selection": {
+ "end": 0,
+ "start": 0,
+ },
+ },
+ },
+ },
+ {
+ "name": "contentSizeChange",
+ "payload": {
+ "nativeEvent": {
+ "contentSize": {
+ "height": 16,
+ "width": 0,
+ },
+ "target": 0,
+ },
+ },
+ },
+ {
+ "name": "endEditing",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ "text": "",
+ },
+ },
+ },
+ {
+ "name": "blur",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ },
+ },
+ },
+]
+`;
+
+exports[`clear() works when not all events have handlers 1`] = `
+[
+ {
+ "name": "changeText",
+ "payload": "",
+ },
+ {
+ "name": "endEditing",
+ "payload": {
+ "nativeEvent": {
+ "target": 0,
+ "text": "",
+ },
+ },
+ },
+]
+`;
diff --git a/src/user-event/__tests__/clear.test.tsx b/src/user-event/__tests__/clear.test.tsx
new file mode 100644
index 000000000..f508df52c
--- /dev/null
+++ b/src/user-event/__tests__/clear.test.tsx
@@ -0,0 +1,217 @@
+import * as React from 'react';
+import { View, TextInput, TextInputProps } from 'react-native';
+import { createEventLogger } from '../../test-utils/events';
+import { render, userEvent } from '../..';
+
+beforeEach(() => {
+ jest.useRealTimers();
+});
+
+function renderTextInputWithToolkit(props: TextInputProps = {}) {
+ const { events, logEvent } = createEventLogger();
+
+ const screen = render(
+
+ );
+
+ const textInput = screen.getByTestId('input');
+
+ return {
+ events,
+ textInput,
+ };
+}
+
+describe('clear()', () => {
+ it('supports basic case', async () => {
+ jest.spyOn(Date, 'now').mockImplementation(() => 100100100100);
+ const { textInput, events } = renderTextInputWithToolkit({
+ value: 'Hello!',
+ });
+
+ const user = userEvent.setup();
+ await user.clear(textInput);
+
+ const eventNames = events.map((e) => e.name);
+ expect(eventNames).toEqual([
+ 'focus',
+ 'selectionChange',
+ 'keyPress',
+ 'change',
+ 'changeText',
+ 'selectionChange',
+ 'endEditing',
+ 'blur',
+ ]);
+
+ expect(events).toMatchSnapshot('value: "Hello!');
+ });
+
+ 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.clear(textInput);
+
+ const eventNames = events.map((e) => e.name);
+ expect(eventNames).toEqual([
+ 'focus',
+ 'selectionChange',
+ 'keyPress',
+ 'change',
+ 'changeText',
+ 'selectionChange',
+ 'endEditing',
+ 'blur',
+ ]);
+ });
+
+ it('supports defaultValue prop', async () => {
+ const { textInput, events } = renderTextInputWithToolkit({
+ defaultValue: 'Hello Default!',
+ });
+
+ const user = userEvent.setup();
+ await user.clear(textInput);
+
+ const eventNames = events.map((e) => e.name);
+ expect(eventNames).toEqual([
+ 'focus',
+ 'selectionChange',
+ 'keyPress',
+ '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();
+ user.clear(textInput);
+
+ expect(textInput.props.value).toBe('Hello!');
+ });
+
+ it('does respect pointer-events prop', async () => {
+ const { textInput } = renderTextInputWithToolkit({
+ value: 'Hello!',
+ pointerEvents: 'none',
+ });
+
+ const user = userEvent.setup();
+ user.clear(textInput);
+
+ 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.clear(textInput);
+
+ const eventNames = events.map((e) => e.name);
+ expect(eventNames).toEqual([
+ 'focus',
+ 'selectionChange',
+ 'keyPress',
+ 'textInput',
+ '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();
+ const screen = render(
+
+ );
+
+ const user = userEvent.setup();
+ await user.clear(screen.getByTestId('input'));
+
+ const eventNames = events.map((e) => e.name);
+ expect(eventNames).toEqual(['changeText', 'endEditing']);
+
+ expect(events).toMatchSnapshot();
+ });
+
+ it('does NOT work on View', async () => {
+ const screen = render();
+
+ const user = userEvent.setup();
+ await expect(
+ user.clear(screen.getByTestId('input'))
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"clear() only supports host "TextInput" elements. Passed element has type: "View"."`
+ );
+ });
+
+ // View that ignores props type checking
+ const AnyView = View as React.ComponentType;
+
+ it('does NOT bubble up', async () => {
+ const parentHandler = jest.fn();
+ const screen = render(
+
+
+
+ );
+
+ const user = userEvent.setup();
+ await user.clear(screen.getByTestId('input'));
+ expect(parentHandler).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts
new file mode 100644
index 000000000..46df2a22d
--- /dev/null
+++ b/src/user-event/clear.ts
@@ -0,0 +1,59 @@
+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 { EventBuilder } from './event-builder';
+import { UserEventInstance } from './setup';
+import { dispatchEvent, wait, isEditableTextInput } from './utils';
+import { emitTypingEvents } from './type/type';
+
+export async function clear(
+ this: UserEventInstance,
+ element: ReactTestInstance
+): Promise {
+ if (!isHostTextInput(element)) {
+ throw new ErrorWithStack(
+ `clear() only supports host "TextInput" elements. Passed element has type: "${element.type}".`,
+ clear
+ );
+ }
+
+ if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
+ return;
+ }
+
+ // 1. Enter element
+ dispatchEvent(element, 'focus', EventBuilder.Common.focus());
+
+ // 2. Select all
+ const previousText = element.props.value ?? element.props.defaultValue ?? '';
+ const selectionRange = {
+ start: 0,
+ end: previousText.length,
+ };
+ dispatchEvent(
+ element,
+ 'selectionChange',
+ EventBuilder.TextInput.selectionChange(selectionRange)
+ );
+
+ // 3. Press backspace
+ const finalText = '';
+ await emitTypingEvents(
+ this.config,
+ element,
+ 'Backspace',
+ finalText,
+ previousText
+ );
+
+ // 4. Exit element
+ await wait(this.config);
+ dispatchEvent(
+ element,
+ 'endEditing',
+ EventBuilder.TextInput.endEditing(finalText)
+ );
+
+ dispatchEvent(element, 'blur', EventBuilder.Common.blur());
+}
diff --git a/src/user-event/index.ts b/src/user-event/index.ts
index dca1719e9..ee4511ad3 100644
--- a/src/user-event/index.ts
+++ b/src/user-event/index.ts
@@ -14,4 +14,5 @@ export const userEvent = {
setup().longPress(element, options),
type: (element: ReactTestInstance, text: string, options?: TypeOptions) =>
setup().type(element, text, options),
+ clear: (element: ReactTestInstance) => setup().clear(element),
};
diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts
index 663d614ad..7c4835cf7 100644
--- a/src/user-event/press/press.ts
+++ b/src/user-event/press/press.ts
@@ -2,13 +2,15 @@ import { ReactTestInstance } from 'react-test-renderer';
import act from '../../act';
import { getHostParent } from '../../helpers/component-tree';
import { isPointerEventEnabled } from '../../helpers/pointer-events';
-import {
- isHostText,
- isHostTextInput,
-} from '../../helpers/host-component-names';
+import { isHostText } from '../../helpers/host-component-names';
import { EventBuilder } from '../event-builder';
import { UserEventConfig, UserEventInstance } from '../setup';
-import { dispatchEvent, wait, warnAboutRealTimersIfNeeded } from '../utils';
+import {
+ dispatchEvent,
+ isEditableTextInput,
+ wait,
+ warnAboutRealTimersIfNeeded,
+} from '../utils';
import { DEFAULT_MIN_PRESS_DURATION } from './constants';
export interface PressOptions {
@@ -51,7 +53,7 @@ const basePress = async (
return;
}
- if (isEnabledTextInput(element)) {
+ if (isEditableTextInput(element) && isPointerEventEnabled(element)) {
await emitTextInputPressEvents(config, element, options);
return;
}
@@ -125,14 +127,6 @@ const isPressableText = (element: ReactTestInstance) => {
);
};
-const isEnabledTextInput = (element: ReactTestInstance) => {
- return (
- isHostTextInput(element) &&
- isPointerEventEnabled(element) &&
- element.props.editable !== false
- );
-};
-
/**
* Dispatches a press event sequence for Text.
*/
diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts
index cc6716c35..db7cbc2dc 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 { PressOptions, press, longPress } from '../press';
import { TypeOptions, type } from '../type';
+import { clear } from '../clear';
export interface UserEventSetupOptions {
/**
@@ -84,7 +85,7 @@ export interface UserEventInstance {
) => Promise;
/**
- * Simulate user pressing on given `TextInput` element and typing given text.
+ * Simulate user pressing on a given `TextInput` element and typing given text.
*
* This method will trigger the events for each character of the text:
* `keyPress`, `change`, `changeText`, `endEditing`, etc.
@@ -92,7 +93,7 @@ export interface UserEventInstance {
* It will also trigger events connected with entering and leaving the text
* input.
*
- * The exact events sent depend on the props of TextInput (`editable`,
+ * The exact events sent depend on the props of the TextInput (`editable`,
* `multiline`, value, defaultValue, etc) and passed options.
*
* @param element TextInput element to type on
@@ -108,6 +109,19 @@ export interface UserEventInstance {
text: string,
options?: TypeOptions
) => Promise;
+
+ /**
+ * Simulate user clearing the text of a given `TextInput` element.
+ *
+ * This method will simulate:
+ * 1. entering TextInput
+ * 2. selecting all text
+ * 3. pressing backspace to delete all text
+ * 4. leaving TextInput
+ *
+ * @param element TextInput element to clear
+ */
+ clear: (element: ReactTestInstance) => Promise;
}
function createInstance(config: UserEventConfig): UserEventInstance {
@@ -120,6 +134,7 @@ function createInstance(config: UserEventConfig): UserEventInstance {
press: press.bind(instance),
longPress: longPress.bind(instance),
type: type.bind(instance),
+ clear: clear.bind(instance),
};
Object.assign(instance, api);
diff --git a/src/user-event/type/__tests__/type-managed.test.tsx b/src/user-event/type/__tests__/type-managed.test.tsx
index c0914b976..178fb6056 100644
--- a/src/user-event/type/__tests__/type-managed.test.tsx
+++ b/src/user-event/type/__tests__/type-managed.test.tsx
@@ -6,7 +6,6 @@ import { userEvent } from '../..';
beforeEach(() => {
jest.useRealTimers();
- jest.clearAllMocks();
});
interface ManagedTextInputProps {
diff --git a/src/user-event/type/__tests__/type.test.tsx b/src/user-event/type/__tests__/type.test.tsx
index 67fb610b1..6b13b1727 100644
--- a/src/user-event/type/__tests__/type.test.tsx
+++ b/src/user-event/type/__tests__/type.test.tsx
@@ -6,7 +6,6 @@ import { userEvent } from '../..';
beforeEach(() => {
jest.useRealTimers();
- jest.clearAllMocks();
});
function renderTextInputWithToolkit(props: TextInputProps = {}) {
diff --git a/src/user-event/type/type.ts b/src/user-event/type/type.ts
index 42534ce86..6ea309376 100644
--- a/src/user-event/type/type.ts
+++ b/src/user-event/type/type.ts
@@ -3,13 +3,8 @@ import { isHostTextInput } from '../../helpers/host-component-names';
import { EventBuilder } from '../event-builder';
import { ErrorWithStack } from '../../helpers/errors';
import { isPointerEventEnabled } from '../../helpers/pointer-events';
-import { UserEventInstance } from '../setup';
-import {
- dispatchEvent,
- wait,
- getTextRange,
- getTextContentSize,
-} from '../utils';
+import { UserEventConfig, UserEventInstance } from '../setup';
+import { dispatchEvent, wait, getTextContentSize } from '../utils';
import { parseKeys } from './parseKeys';
@@ -54,12 +49,16 @@ export async function type(
const previousText = element.props.value ?? currentText;
currentText = applyKey(previousText, key);
- await wait(this.config);
- emitTypingEvents(element, key, currentText, previousText);
+ await emitTypingEvents(
+ this.config,
+ element,
+ key,
+ currentText,
+ previousText
+ );
}
const finalText = element.props.value ?? currentText;
-
await wait(this.config);
if (options?.submitEditing) {
@@ -79,7 +78,8 @@ export async function type(
dispatchEvent(element, 'blur', EventBuilder.Common.blur());
}
-async function emitTypingEvents(
+export async function emitTypingEvents(
+ config: UserEventConfig,
element: ReactTestInstance,
key: string,
currentText: string,
@@ -87,6 +87,7 @@ async function emitTypingEvents(
) {
const isMultiline = element.props.multiline === true;
+ await wait(config);
dispatchEvent(element, 'keyPress', EventBuilder.TextInput.keyPress(key));
// According to the docs only multiline TextInput emits textInput event
@@ -100,10 +101,12 @@ async function emitTypingEvents(
}
dispatchEvent(element, 'change', EventBuilder.TextInput.change(currentText));
-
dispatchEvent(element, 'changeText', currentText);
- const selectionRange = getTextRange(currentText);
+ const selectionRange = {
+ start: currentText.length,
+ end: currentText.length,
+ };
dispatchEvent(
element,
'selectionChange',
diff --git a/src/user-event/utils/__tests__/wait.test.ts b/src/user-event/utils/__tests__/wait.test.ts
index 2606bcd4f..ac89070fc 100644
--- a/src/user-event/utils/__tests__/wait.test.ts
+++ b/src/user-event/utils/__tests__/wait.test.ts
@@ -2,7 +2,6 @@ import { wait } from '../wait';
beforeEach(() => {
jest.useRealTimers();
- jest.clearAllMocks();
});
describe('wait()', () => {
diff --git a/src/user-event/utils/host-components.ts b/src/user-event/utils/host-components.ts
new file mode 100644
index 000000000..1a43785be
--- /dev/null
+++ b/src/user-event/utils/host-components.ts
@@ -0,0 +1,6 @@
+import { ReactTestInstance } from 'react-test-renderer';
+import { isHostTextInput } from '../../helpers/host-component-names';
+
+export function isEditableTextInput(element: ReactTestInstance) {
+ return isHostTextInput(element) && element.props.editable !== false;
+}
diff --git a/src/user-event/utils/index.ts b/src/user-event/utils/index.ts
index 56e00613b..d97431dae 100644
--- a/src/user-event/utils/index.ts
+++ b/src/user-event/utils/index.ts
@@ -1,5 +1,6 @@
export * from './content-size';
export * from './dispatch-event';
+export * from './host-components';
export * from './text-range';
export * from './wait';
export * from './warn-about-real-timers';
diff --git a/src/user-event/utils/text-range.ts b/src/user-event/utils/text-range.ts
index 05740ecee..31a2cf593 100644
--- a/src/user-event/utils/text-range.ts
+++ b/src/user-event/utils/text-range.ts
@@ -2,10 +2,3 @@ export interface TextRange {
start: number;
end: number;
}
-
-export function getTextRange(text: string): TextRange {
- return {
- start: text.length,
- end: text.length,
- };
-}
diff --git a/website/docs/UserEvent.md b/website/docs/UserEvent.md
index 55178e351..6c799d937 100644
--- a/website/docs/UserEvent.md
+++ b/website/docs/UserEvent.md
@@ -14,6 +14,8 @@ title: User Event
- [`type()`](#type)
- [Options](#options-2)
- [Sequence of events](#sequence-of-events)
+- [`clear()`](#clear)
+ - [Sequence of events](#sequence-of-events-1)
:::caution
User Event API is in beta stage.
@@ -108,6 +110,10 @@ This helper simulates user focusing on `TextInput` element, typing `text` one ch
This function supports only host `TextInput` elements. Passing other element type will result in throwing error.
+:::note
+This function will add text to the text already present in the text input (as specified by `value` or `defaultValue` props). In order to replace existing text, use [`clear()`](#clear) helper first.
+:::
+
### Options
- `skipPress` - if true, `pressIn` and `pressOut` events will not be triggered.
- `submitEditing` - if true, `submitEditing` event will be triggered after typing the text.
@@ -141,3 +147,45 @@ The `textInput` event is sent only for mutliline text inputs.
The `submitEditing` event is skipped by default. It can sent by setting `submitEditing: true` option.
+## `clear()`
+
+```ts
+clear(
+ element: ReactTestInstance,
+}
+```
+
+Example
+```ts
+const user = userEvent.setup();
+await user.clear(textInput);
+```
+
+This helper simulates user clearing content of `TextInput` element.
+
+This function supports only host `TextInput` elements. Passing other element type will result in throwing error.
+
+### Sequence of events
+
+The sequence of events depends on `multiline` prop, as well as passed options.
+
+Events will not be emitted if `editable` prop is set to `false`.
+
+**Entering the element**:
+- `focus`
+
+**Selecting all content**:
+- `selectionChange`
+
+**Pressing backspace**:
+- `keyPress`
+- `textInput` (optional)
+- `change`
+- `changeText`
+- `selectionChange`
+
+The `textInput` event is sent only for mutliline text inputs.
+
+**Leaving the element**:
+- `endEditing`
+- `blur`