From d37100facb2954059712d85718be5a1ee7c82391 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 7 Aug 2023 11:18:28 +0200 Subject: [PATCH 1/8] feat: implement clear() --- .../__snapshots__/clear.test.tsx.snap | 269 ++++++++++++++++++ src/user-event/__tests__/clear.test.tsx | 206 ++++++++++++++ src/user-event/clear.ts | 60 ++++ src/user-event/index.ts | 1 + src/user-event/setup/setup.ts | 19 +- src/user-event/type/type.ts | 29 +- src/user-event/utils/text-range.ts | 7 - 7 files changed, 569 insertions(+), 22 deletions(-) create mode 100644 src/user-event/__tests__/__snapshots__/clear.test.tsx.snap create mode 100644 src/user-event/__tests__/clear.test.tsx create mode 100644 src/user-event/clear.ts 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..9eb3b9d20 --- /dev/null +++ b/src/user-event/__tests__/clear.test.tsx @@ -0,0 +1,206 @@ +import * as React from 'react'; +import { View, TextInput, TextInputProps } from 'react-native'; +import { createEventLogger } from '../../test-utils/events'; +import { render } from '../../..'; +import { userEvent } from '../..'; + +beforeEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); +}); + +function renderTextInputWithToolkit(props: TextInputProps = {}) { + const { events, logEvent } = createEventLogger(); + + const screen = render( + + ); + + return { + ...screen, + events, + }; +} + +describe('clear()', () => { + it('supports basic case', async () => { + jest.spyOn(Date, 'now').mockImplementation(() => 100100100100); + const { events, ...queries } = renderTextInputWithToolkit({ + value: 'Hello!', + }); + + const user = userEvent.setup(); + await user.clear(queries.getByTestId('input')); + + 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 { events, ...queries } = renderTextInputWithToolkit({ + value: 'Hello!', + }); + + const user = userEvent.setup(); + await user.clear(queries.getByTestId('input')); + + const eventNames = events.map((e) => e.name); + expect(eventNames).toEqual([ + 'focus', + 'selectionChange', + 'keyPress', + 'change', + 'changeText', + 'selectionChange', + 'endEditing', + 'blur', + ]); + }); + + it('supports defaultValue prop', async () => { + const { events, ...queries } = renderTextInputWithToolkit({ + defaultValue: 'Hello Default!', + }); + + const user = userEvent.setup(); + await user.clear(queries.getByTestId('input')); + + 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 { events, ...queries } = renderTextInputWithToolkit({ + value: 'Hello!', + editable: false, + }); + + const user = userEvent.setup(); + await user.clear(queries.getByTestId('input')); + + const eventNames = events.map((e) => e.name); + expect(eventNames).toEqual([]); + }); + + it('supports multiline', async () => { + const { events, ...queries } = renderTextInputWithToolkit({ + value: 'Hello World!\nHow are you?', + multiline: true, + }); + + const user = userEvent.setup(); + await user.clear(queries.getByTestId('input')); + + 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() works only with 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..aaec7f6b7 --- /dev/null +++ b/src/user-event/clear.ts @@ -0,0 +1,60 @@ +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 } from './utils'; +import { emitTypingEvents } from './type/type'; + +export async function clear( + this: UserEventInstance, + element: ReactTestInstance +): Promise { + if (!isHostTextInput(element)) { + throw new ErrorWithStack( + `clear() works only with host "TextInput" elements. Passed element has type "${element.type}".`, + clear + ); + } + + // Skip events if the element is disabled + if (element.props.editable === false || !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/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/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/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, - }; -} From 8bc328300ab09142b164b70d5694910c1dff0669 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 7 Aug 2023 11:26:17 +0200 Subject: [PATCH 2/8] chore: add docs --- website/docs/UserEvent.md | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) 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` From 18f4a7bb499d0fd2d27b3db540ca8ccd772f0a07 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 7 Aug 2023 11:34:24 +0200 Subject: [PATCH 3/8] chore: fix lint --- src/user-event/__tests__/clear.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/user-event/__tests__/clear.test.tsx b/src/user-event/__tests__/clear.test.tsx index 9eb3b9d20..b9c214629 100644 --- a/src/user-event/__tests__/clear.test.tsx +++ b/src/user-event/__tests__/clear.test.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import { View, TextInput, TextInputProps } from 'react-native'; import { createEventLogger } from '../../test-utils/events'; -import { render } from '../../..'; -import { userEvent } from '../..'; +import { render, userEvent } from '../..'; beforeEach(() => { jest.useRealTimers(); From 5058bff09332378c23abd7ba8703e61a21913403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 7 Aug 2023 14:59:10 +0200 Subject: [PATCH 4/8] refactor: remove redundant clearAllMocks --- src/user-event/__tests__/clear.test.tsx | 1 - src/user-event/type/__tests__/type-managed.test.tsx | 1 - src/user-event/type/__tests__/type.test.tsx | 1 - src/user-event/utils/__tests__/wait.test.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/src/user-event/__tests__/clear.test.tsx b/src/user-event/__tests__/clear.test.tsx index b9c214629..323d1264b 100644 --- a/src/user-event/__tests__/clear.test.tsx +++ b/src/user-event/__tests__/clear.test.tsx @@ -5,7 +5,6 @@ import { render, userEvent } from '../..'; beforeEach(() => { jest.useRealTimers(); - jest.clearAllMocks(); }); function renderTextInputWithToolkit(props: TextInputProps = {}) { 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/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()', () => { From ecdb6497cc8816ebf1f60b2cea09e5b2b7db3b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 8 Aug 2023 11:56:48 +0200 Subject: [PATCH 5/8] feat: throw on disabled element --- src/user-event/__tests__/clear.test.tsx | 47 ++++++++++++++++++------- src/user-event/clear.ts | 23 +++++++++--- src/user-event/utils/host-components.ts | 11 ++++++ src/user-event/utils/index.ts | 1 + 4 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 src/user-event/utils/host-components.ts diff --git a/src/user-event/__tests__/clear.test.tsx b/src/user-event/__tests__/clear.test.tsx index 323d1264b..c140ae1b0 100644 --- a/src/user-event/__tests__/clear.test.tsx +++ b/src/user-event/__tests__/clear.test.tsx @@ -29,21 +29,23 @@ function renderTextInputWithToolkit(props: TextInputProps = {}) { /> ); + const textInput = screen.getByTestId('input'); + return { - ...screen, events, + textInput, }; } describe('clear()', () => { it('supports basic case', async () => { jest.spyOn(Date, 'now').mockImplementation(() => 100100100100); - const { events, ...queries } = renderTextInputWithToolkit({ + const { textInput, events } = renderTextInputWithToolkit({ value: 'Hello!', }); const user = userEvent.setup(); - await user.clear(queries.getByTestId('input')); + await user.clear(textInput); const eventNames = events.map((e) => e.name); expect(eventNames).toEqual([ @@ -62,12 +64,12 @@ describe('clear()', () => { it.each(['modern', 'legacy'])('works with %s fake timers', async (type) => { jest.useFakeTimers({ legacyFakeTimers: type === 'legacy' }); - const { events, ...queries } = renderTextInputWithToolkit({ + const { textInput, events } = renderTextInputWithToolkit({ value: 'Hello!', }); const user = userEvent.setup(); - await user.clear(queries.getByTestId('input')); + await user.clear(textInput); const eventNames = events.map((e) => e.name); expect(eventNames).toEqual([ @@ -83,12 +85,12 @@ describe('clear()', () => { }); it('supports defaultValue prop', async () => { - const { events, ...queries } = renderTextInputWithToolkit({ + const { textInput, events } = renderTextInputWithToolkit({ defaultValue: 'Hello Default!', }); const user = userEvent.setup(); - await user.clear(queries.getByTestId('input')); + await user.clear(textInput); const eventNames = events.map((e) => e.name); expect(eventNames).toEqual([ @@ -106,26 +108,45 @@ describe('clear()', () => { }); it('does respect editable prop', async () => { - const { events, ...queries } = renderTextInputWithToolkit({ + const { textInput } = renderTextInputWithToolkit({ value: 'Hello!', editable: false, }); const user = userEvent.setup(); - await user.clear(queries.getByTestId('input')); + await expect( + user.clear(textInput) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"clear() works only on editable "TextInput" elements."` + ); - const eventNames = events.map((e) => e.name); - expect(eventNames).toEqual([]); + 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 expect( + user.clear(textInput) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"clear() works only on focusable "TextInput" elements."` + ); + + expect(textInput.props.value).toBe('Hello!'); }); it('supports multiline', async () => { - const { events, ...queries } = renderTextInputWithToolkit({ + const { textInput, events } = renderTextInputWithToolkit({ value: 'Hello World!\nHow are you?', multiline: true, }); const user = userEvent.setup(); - await user.clear(queries.getByTestId('input')); + await user.clear(textInput); const eventNames = events.map((e) => e.name); expect(eventNames).toEqual([ diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts index aaec7f6b7..a6991dad5 100644 --- a/src/user-event/clear.ts +++ b/src/user-event/clear.ts @@ -1,10 +1,14 @@ 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 } from './utils'; +import { + dispatchEvent, + wait, + isEditableTextInput, + isFocusableTextInput, +} from './utils'; import { emitTypingEvents } from './type/type'; export async function clear( @@ -18,9 +22,18 @@ export async function clear( ); } - // Skip events if the element is disabled - if (element.props.editable === false || !isPointerEventEnabled(element)) { - return; + if (!isEditableTextInput(element)) { + throw new ErrorWithStack( + `clear() works only on editable "TextInput" elements.`, + clear + ); + } + + if (!isFocusableTextInput(element)) { + throw new ErrorWithStack( + `clear() works only on focusable "TextInput" elements.`, + clear + ); } // 1. Enter element diff --git a/src/user-event/utils/host-components.ts b/src/user-event/utils/host-components.ts new file mode 100644 index 000000000..6dc4fb96e --- /dev/null +++ b/src/user-event/utils/host-components.ts @@ -0,0 +1,11 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import { isPointerEventEnabled } from '../../helpers/pointer-events'; +import { isHostTextInput } from '../../helpers/host-component-names'; + +export function isEditableTextInput(element: ReactTestInstance) { + return isHostTextInput(element) && element.props.editable !== false; +} + +export function isFocusableTextInput(element: ReactTestInstance) { + return isHostTextInput(element) && isPointerEventEnabled(element); +} 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'; From 12ae1baef87239fbff5c46bd8f8711894c074c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 8 Aug 2023 13:05:57 +0200 Subject: [PATCH 6/8] refactor: tweak errors --- src/user-event/__tests__/clear.test.tsx | 6 +++--- src/user-event/clear.ts | 19 ++++++------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/user-event/__tests__/clear.test.tsx b/src/user-event/__tests__/clear.test.tsx index c140ae1b0..5229bc984 100644 --- a/src/user-event/__tests__/clear.test.tsx +++ b/src/user-event/__tests__/clear.test.tsx @@ -117,7 +117,7 @@ describe('clear()', () => { await expect( user.clear(textInput) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"clear() works only on editable "TextInput" elements."` + `"clear() only supports editable elements."` ); expect(textInput.props.value).toBe('Hello!'); @@ -133,7 +133,7 @@ describe('clear()', () => { await expect( user.clear(textInput) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"clear() works only on focusable "TextInput" elements."` + `"clear() cannot focus on given element due to "pointerEvents" prop."` ); expect(textInput.props.value).toBe('Hello!'); @@ -193,7 +193,7 @@ describe('clear()', () => { await expect( user.clear(screen.getByTestId('input')) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"clear() works only with host "TextInput" elements. Passed element has type "View"."` + `"clear() only supports host "TextInput" elements. Passed element has type: "View"."` ); }); diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts index a6991dad5..c17694adc 100644 --- a/src/user-event/clear.ts +++ b/src/user-event/clear.ts @@ -1,14 +1,10 @@ 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, - isFocusableTextInput, -} from './utils'; +import { dispatchEvent, wait, isEditableTextInput } from './utils'; import { emitTypingEvents } from './type/type'; export async function clear( @@ -17,21 +13,18 @@ export async function clear( ): Promise { if (!isHostTextInput(element)) { throw new ErrorWithStack( - `clear() works only with host "TextInput" elements. Passed element has type "${element.type}".`, + `clear() only supports host "TextInput" elements. Passed element has type: "${element.type}".`, clear ); } if (!isEditableTextInput(element)) { - throw new ErrorWithStack( - `clear() works only on editable "TextInput" elements.`, - clear - ); + throw new ErrorWithStack('clear() only supports editable elements.', clear); } - if (!isFocusableTextInput(element)) { + if (!isPointerEventEnabled(element)) { throw new ErrorWithStack( - `clear() works only on focusable "TextInput" elements.`, + 'clear() cannot focus on given element due to "pointerEvents" prop.', clear ); } From 109dc4ceb7add03f791e28c2567e765b34cf9a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 8 Aug 2023 13:21:28 +0200 Subject: [PATCH 7/8] refactor: cleanup --- src/user-event/press/press.ts | 22 ++++++++-------------- src/user-event/utils/host-components.ts | 5 ----- 2 files changed, 8 insertions(+), 19 deletions(-) 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/utils/host-components.ts b/src/user-event/utils/host-components.ts index 6dc4fb96e..1a43785be 100644 --- a/src/user-event/utils/host-components.ts +++ b/src/user-event/utils/host-components.ts @@ -1,11 +1,6 @@ import { ReactTestInstance } from 'react-test-renderer'; -import { isPointerEventEnabled } from '../../helpers/pointer-events'; import { isHostTextInput } from '../../helpers/host-component-names'; export function isEditableTextInput(element: ReactTestInstance) { return isHostTextInput(element) && element.props.editable !== false; } - -export function isFocusableTextInput(element: ReactTestInstance) { - return isHostTextInput(element) && isPointerEventEnabled(element); -} From e0c15ea3d747b4bd23fa614199e32bed1490c8a6 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 10 Aug 2023 22:30:59 +0200 Subject: [PATCH 8/8] refactor: remove error on disabled element --- src/user-event/__tests__/clear.test.tsx | 12 ++---------- src/user-event/clear.ts | 11 ++--------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/user-event/__tests__/clear.test.tsx b/src/user-event/__tests__/clear.test.tsx index 5229bc984..f508df52c 100644 --- a/src/user-event/__tests__/clear.test.tsx +++ b/src/user-event/__tests__/clear.test.tsx @@ -114,11 +114,7 @@ describe('clear()', () => { }); const user = userEvent.setup(); - await expect( - user.clear(textInput) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"clear() only supports editable elements."` - ); + user.clear(textInput); expect(textInput.props.value).toBe('Hello!'); }); @@ -130,11 +126,7 @@ describe('clear()', () => { }); const user = userEvent.setup(); - await expect( - user.clear(textInput) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"clear() cannot focus on given element due to "pointerEvents" prop."` - ); + user.clear(textInput); expect(textInput.props.value).toBe('Hello!'); }); diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts index c17694adc..46df2a22d 100644 --- a/src/user-event/clear.ts +++ b/src/user-event/clear.ts @@ -18,15 +18,8 @@ export async function clear( ); } - if (!isEditableTextInput(element)) { - throw new ErrorWithStack('clear() only supports editable elements.', clear); - } - - if (!isPointerEventEnabled(element)) { - throw new ErrorWithStack( - 'clear() cannot focus on given element due to "pointerEvents" prop.', - clear - ); + if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) { + return; } // 1. Enter element