diff --git a/src/user-event/__tests__/__snapshots__/clear.test.tsx.snap b/src/user-event/__tests__/__snapshots__/clear.test.tsx.snap index 5bf605ec..33523afc 100644 --- a/src/user-event/__tests__/__snapshots__/clear.test.tsx.snap +++ b/src/user-event/__tests__/__snapshots__/clear.test.tsx.snap @@ -1,5 +1,107 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`clear() skips blur and endEditing events when \`skipBlur: true\`: value: "Hello!" skipBlur: 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": 6, + "start": 0, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "Backspace", + }, + "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": "", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "", + }, + { + "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, + }, + }, +] +`; + exports[`clear() supports basic case: value: "Hello! 1`] = ` [ { diff --git a/src/user-event/__tests__/clear.test.tsx b/src/user-event/__tests__/clear.test.tsx index 712e73df..568c2a0a 100644 --- a/src/user-event/__tests__/clear.test.tsx +++ b/src/user-event/__tests__/clear.test.tsx @@ -219,4 +219,45 @@ describe('clear()', () => { await user.clear(input); expect(input).toHaveDisplayValue(''); }); + + it('skips blur and endEditing events when `skipBlur: true`', async () => { + const { textInput, events } = renderTextInputWithToolkit({ + value: 'Hello!', + }); + + const user = userEvent.setup(); + await user.clear(textInput, { + skipBlur: true, + }); + + const eventNames = getEventsNames(events); + + // Ensure 'endEditing' and 'blur' are not present + expect(eventNames).not.toContain('endEditing'); + expect(eventNames).not.toContain('blur'); + + // Verify the exact events that should be present + expect(eventNames).toEqual([ + 'focus', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + ]); + + expect(events).toMatchSnapshot('value: "Hello!" skipBlur: true'); + }); + + it('sets native state value for unmanaged text inputs when `skipBlur: true`', async () => { + render(); + + const user = userEvent.setup(); + const input = screen.getByTestId('input'); + await user.type(input, 'abc'); + expect(input).toHaveDisplayValue('abc'); + + await user.clear(input, { skipBlur: true }); + expect(input).toHaveDisplayValue(''); + }); }); diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts index 4a070187..b9bd02a8 100644 --- a/src/user-event/clear.ts +++ b/src/user-event/clear.ts @@ -9,7 +9,15 @@ import type { UserEventInstance } from './setup'; import { emitTypingEvents } from './type/type'; import { dispatchEvent, wait } from './utils'; -export async function clear(this: UserEventInstance, element: ReactTestInstance): Promise { +export interface BlurOptions { + skipBlur?: boolean; +} + +export async function clear( + this: UserEventInstance, + element: ReactTestInstance, + options?: BlurOptions, +): Promise { if (!isHostTextInput(element)) { throw new ErrorWithStack( `clear() only supports host "TextInput" elements. Passed element has type: "${element.type}".`, @@ -45,7 +53,9 @@ export async function clear(this: UserEventInstance, element: ReactTestInstance) }); // 4. Exit element - await wait(this.config); - await dispatchEvent(element, 'endEditing', EventBuilder.TextInput.endEditing(emptyText)); - await dispatchEvent(element, 'blur', EventBuilder.Common.blur()); + if (!options?.skipBlur) { + await wait(this.config); + await dispatchEvent(element, 'endEditing', EventBuilder.TextInput.endEditing(emptyText)); + await dispatchEvent(element, 'blur', EventBuilder.Common.blur()); + } } diff --git a/src/user-event/index.ts b/src/user-event/index.ts index a94f54be..4c87b1b2 100644 --- a/src/user-event/index.ts +++ b/src/user-event/index.ts @@ -1,5 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; +import type { BlurOptions } from './clear'; import type { PressOptions } from './press'; import type { ScrollToOptions } from './scroll'; import { setup } from './setup'; @@ -16,7 +17,7 @@ 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), + clear: (element: ReactTestInstance, options?: BlurOptions) => setup().clear(element, options), paste: (element: ReactTestInstance, text: string) => setup().paste(element, text), scrollTo: (element: ReactTestInstance, options: ScrollToOptions) => setup().scrollTo(element, options), diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts index e6d164e5..27be30ce 100644 --- a/src/user-event/setup/setup.ts +++ b/src/user-event/setup/setup.ts @@ -2,6 +2,7 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { jestFakeTimersAreEnabled } from '../../helpers/timers'; import { wrapAsync } from '../../helpers/wrap-async'; +import type { BlurOptions } from '../clear'; import { clear } from '../clear'; import { paste } from '../paste'; import type { PressOptions } from '../press'; @@ -122,7 +123,7 @@ export interface UserEventInstance { * * @param element TextInput element to clear */ - clear: (element: ReactTestInstance) => Promise; + clear: (element: ReactTestInstance, options?: BlurOptions) => Promise; /** * Simulate user pasting the text to a given `TextInput` element. diff --git a/src/user-event/type/__tests__/__snapshots__/type-managed.test.tsx.snap b/src/user-event/type/__tests__/__snapshots__/type-managed.test.tsx.snap index 3d0383fc..e19c3e06 100644 --- a/src/user-event/type/__tests__/__snapshots__/type-managed.test.tsx.snap +++ b/src/user-event/type/__tests__/__snapshots__/type-managed.test.tsx.snap @@ -1,5 +1,496 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`type() for managed TextInput clears existing text when \`clearBefore: true\` in managed TextInput: input: "World", initialValue: "Hello!", clearBefore: true 1`] = ` +[ + { + "name": "pressIn", + "payload": { + "currentTarget": { + "measure": [Function], + }, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, + "timestamp": 100100100100, + "touches": [], + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "focus", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "target": 0, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "pressOut", + "payload": { + "currentTarget": { + "measure": [Function], + }, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, + "timestamp": 100100100100, + "touches": [], + }, + "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": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "Backspace", + }, + "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": "", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "", + }, + { + "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": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "W", + }, + "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": "W", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "W", + }, + { + "name": "selectionChange", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "selection": { + "end": 1, + "start": 1, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "o", + }, + "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": "Wo", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "Wo", + }, + { + "name": "selectionChange", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "selection": { + "end": 2, + "start": 2, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "r", + }, + "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": "Wor", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "Wor", + }, + { + "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": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "l", + }, + "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": "Worl", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "Worl", + }, + { + "name": "selectionChange", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "selection": { + "end": 4, + "start": 4, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "d", + }, + "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": "World", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "World", + }, + { + "name": "selectionChange", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "selection": { + "end": 5, + "start": 5, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "endEditing", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "target": 0, + "text": "World", + }, + "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[`type() for managed TextInput supports basic case: input: "Wow" 1`] = ` [ { diff --git a/src/user-event/type/__tests__/__snapshots__/type.test.tsx.snap b/src/user-event/type/__tests__/__snapshots__/type.test.tsx.snap index a7830825..932a1b22 100644 --- a/src/user-event/type/__tests__/__snapshots__/type.test.tsx.snap +++ b/src/user-event/type/__tests__/__snapshots__/type.test.tsx.snap @@ -1,5 +1,496 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`type() clears existing text when \`clearBefore: true\`: input: "World", defaultValue: "Hello!", clearBefore: true 1`] = ` +[ + { + "name": "pressIn", + "payload": { + "currentTarget": { + "measure": [Function], + }, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, + "timestamp": 100100100100, + "touches": [], + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "focus", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "target": 0, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "pressOut", + "payload": { + "currentTarget": { + "measure": [Function], + }, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, + "timestamp": 100100100100, + "touches": [], + }, + "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": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "Backspace", + }, + "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": "", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "", + }, + { + "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": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "W", + }, + "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": "W", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "W", + }, + { + "name": "selectionChange", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "selection": { + "end": 1, + "start": 1, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "o", + }, + "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": "Wo", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "Wo", + }, + { + "name": "selectionChange", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "selection": { + "end": 2, + "start": 2, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "r", + }, + "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": "Wor", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "Wor", + }, + { + "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": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "l", + }, + "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": "Worl", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "Worl", + }, + { + "name": "selectionChange", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "selection": { + "end": 4, + "start": 4, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "keyPress", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "key": "d", + }, + "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": "World", + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "changeText", + "payload": "World", + }, + { + "name": "selectionChange", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "selection": { + "end": 5, + "start": 5, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "endEditing", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "target": 0, + "text": "World", + }, + "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[`type() supports backspace: input: "{Backspace}a", defaultValue: "xxx" 1`] = ` [ { diff --git a/src/user-event/type/__tests__/type-managed.test.tsx b/src/user-event/type/__tests__/type-managed.test.tsx index 8f567784..4d4d21ee 100644 --- a/src/user-event/type/__tests__/type-managed.test.tsx +++ b/src/user-event/type/__tests__/type-managed.test.tsx @@ -138,4 +138,50 @@ describe('type() for managed TextInput', () => { 'selectionChange', ]); }); + + it('clears existing text when `clearBefore: true` in managed TextInput', async () => { + jest.spyOn(Date, 'now').mockImplementation(() => 100100100100); + const { events, logEvent } = createEventLogger(); + render(); + + const user = userEvent.setup(); + await user.type(screen.getByTestId('input'), 'World', { + clearBefore: true, + }); + + expect(getEventsNames(events)).toEqual([ + 'pressIn', + 'focus', + 'pressOut', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'endEditing', + 'blur', + ]); + + expect(events).toMatchSnapshot('input: "World", initialValue: "Hello!", clearBefore: true'); + }); }); diff --git a/src/user-event/type/__tests__/type.test.tsx b/src/user-event/type/__tests__/type.test.tsx index 7ce49900..33acb893 100644 --- a/src/user-event/type/__tests__/type.test.tsx +++ b/src/user-event/type/__tests__/type.test.tsx @@ -418,4 +418,58 @@ describe('type()', () => { nativeEvent: { selection: { start: 1, end: 1 } }, }); }); + + it('clears existing text when `clearBefore: true`', async () => { + const { events } = renderTextInputWithToolkit({ + defaultValue: 'Hello!', + }); + + const user = userEvent.setup(); + await user.type(screen.getByTestId('input'), 'World', { + clearBefore: true, + }); + + expect(getEventsNames(events)).toEqual([ + 'pressIn', + 'focus', + 'pressOut', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'endEditing', + 'blur', + ]); + + expect(lastEventPayload(events, 'changeText')).toBe('World'); + expect(lastEventPayload(events, 'endEditing')).toMatchObject({ + nativeEvent: { + target: 0, + text: 'World', + }, + }); + + expect(events).toMatchSnapshot('input: "World", defaultValue: "Hello!", clearBefore: true'); + }); }); diff --git a/src/user-event/type/type.ts b/src/user-event/type/type.ts index 8607ef87..58648dad 100644 --- a/src/user-event/type/type.ts +++ b/src/user-event/type/type.ts @@ -14,6 +14,7 @@ export interface TypeOptions { skipPress?: boolean; submitEditing?: boolean; skipBlur?: boolean; + clearBefore?: boolean; } export async function type( @@ -47,6 +48,26 @@ export async function type( await dispatchEvent(element, 'pressOut', EventBuilder.Common.touch()); } + if (options?.clearBefore) { + const textToClear = getTextInputValue(element); + const selectionRange = { + start: 0, + end: textToClear.length, + }; + await dispatchEvent( + element, + 'selectionChange', + EventBuilder.TextInput.selectionChange(selectionRange), + ); + + const emptyText = ''; + await emitTypingEvents(element, { + config: this.config, + key: 'Backspace', + text: emptyText, + }); + } + let currentText = getTextInputValue(element); for (const key of keys) { const previousText = getTextInputValue(element); diff --git a/website/docs/13.x/docs/api/events/user-event.mdx b/website/docs/13.x/docs/api/events/user-event.mdx index 4684faeb..543d0b7d 100644 --- a/website/docs/13.x/docs/api/events/user-event.mdx +++ b/website/docs/13.x/docs/api/events/user-event.mdx @@ -83,6 +83,7 @@ type( skipPress?: boolean; skipBlur?: boolean; submitEditing?: boolean; + clearBefore?: boolean; } ``` @@ -98,7 +99,7 @@ This helper simulates the user focusing on a `TextInput` element, typing `text` This function supports only host `TextInput` elements. Passing other element types will result in throwing an error. :::note -This function will add text to the text already present in the text input (as specified by `value` or `defaultValue` props). To replace existing text, use [`clear()`](#clear) helper first. +This function will add text to the text already present in the text input (as specified by `value` or `defaultValue` props). To replace existing text, use the `clearBefore` option or use the [`clear()`](#clear) helper first. ::: ### Options {#type-options} @@ -106,6 +107,7 @@ This function will add text to the text already present in the text input (as sp - `skipPress` - if true, `pressIn` and `pressOut` events will not be triggered. - `skipBlur` - if true, `endEditing` and `blur` events will not be triggered when typing is complete. - `submitEditing` - if true, `submitEditing` event will be triggered after typing the text. +- `clearBefore` - if true, the existing text in the input will be cleared before typing the new text. ### Sequence of events {#type-sequence} @@ -143,6 +145,9 @@ The `endEditing` and `blur` events can be skipped by passing the `skipBlur: true ```ts clear( element: ReactTestInstance, + options?: { + skipBlur?: boolean; + } ) ``` @@ -157,6 +162,10 @@ This helper simulates the user clearing the content of a `TextInput` element. This function supports only host `TextInput` elements. Passing other element types will result in throwing an error. +### Options {#clear-options} + +- `skipBlur` - if true, `endEditing` and `blur` events will not be triggered after clearing the text. + ### Sequence of events {#clear-sequence} Events will not be emitted if the `editable` prop is set to `false`.