diff --git a/src/fire-event.ts b/src/fire-event.ts index ac7d8802b..98c7745a3 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -173,10 +173,10 @@ function setNativeStateIfNeeded(element: ReactTestInstance, eventName: string, v } } -function tryGetContentOffset(value: unknown): Point | null { +function tryGetContentOffset(event: unknown): Point | null { try { // @ts-expect-error: try to extract contentOffset from the event value - const contentOffset = value?.nativeEvent?.contentOffset; + const contentOffset = event?.nativeEvent?.contentOffset; const x = contentOffset?.x; const y = contentOffset?.y; if (typeof x === 'number' || typeof y === 'number') { diff --git a/src/matchers/types.ts b/src/matchers/types.ts index e2b0ead01..1aed70110 100644 --- a/src/matchers/types.ts +++ b/src/matchers/types.ts @@ -218,7 +218,7 @@ export interface JestNativeMatchers { toHaveAccessibleName(expectedName?: TextMatch, options?: TextMatchOptions): R; /** - * Assert whether a host `TextInput` element has a given display value based on `value` and `defaultValue` props. + * Assert whether a host `TextInput` element has a given display value based on `value` prop, unmanaged native state, and `defaultValue` prop. * * @see * [Jest Matchers docs](https://callstack.github.io/react-native-testing-library/docs/jest-matchers#tohavedisplayvalue) diff --git a/src/queries/__tests__/display-value.test.tsx b/src/queries/__tests__/display-value.test.tsx index aa63b2391..fb8d5e68c 100644 --- a/src/queries/__tests__/display-value.test.tsx +++ b/src/queries/__tests__/display-value.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { TextInput, View } from 'react-native'; - -import { render, screen } from '../..'; +import { fireEvent, render, screen } from '../..'; +import '../../matchers/extend-expect'; const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; const PLACEHOLDER_CHEF = 'Who inspected freshness?'; @@ -30,13 +30,12 @@ const Banana = () => ( test('getByDisplayValue, queryByDisplayValue', () => { render(); - const input = screen.getByDisplayValue(/custom/i); - expect(input.props.value).toBe(INPUT_FRESHNESS); + const input = screen.getByDisplayValue(/custom/i); + expect(input).toHaveDisplayValue(INPUT_FRESHNESS); const sameInput = screen.getByDisplayValue(INPUT_FRESHNESS); - - expect(sameInput.props.value).toBe(INPUT_FRESHNESS); + expect(sameInput).toHaveDisplayValue(INPUT_FRESHNESS); expect(() => screen.getByDisplayValue('no value')).toThrow( 'Unable to find an element with displayValue: no value', ); @@ -203,3 +202,13 @@ test('error message renders the element tree, preserving only helpful props', as />" `); }); + +test('supports unmanaged TextInput element', () => { + render(); + + const input = screen.getByDisplayValue(''); + expect(input).toHaveDisplayValue(''); + + fireEvent.changeText(input, 'Hello!'); + expect(input).toHaveDisplayValue('Hello!'); +}); diff --git a/src/user-event/__tests__/clear.test.tsx b/src/user-event/__tests__/clear.test.tsx index 27d713a1d..c7f4b1d95 100644 --- a/src/user-event/__tests__/clear.test.tsx +++ b/src/user-event/__tests__/clear.test.tsx @@ -113,7 +113,7 @@ describe('clear()', () => { const user = userEvent.setup(); await user.clear(textInput); - expect(textInput.props.value).toBe('Hello!'); + expect(textInput).toHaveDisplayValue('Hello!'); }); it('does respect pointer-events prop', async () => { @@ -125,7 +125,7 @@ describe('clear()', () => { const user = userEvent.setup(); await user.clear(textInput); - expect(textInput.props.value).toBe('Hello!'); + expect(textInput).toHaveDisplayValue('Hello!'); }); it('supports multiline', async () => { diff --git a/src/user-event/__tests__/paste.test.tsx b/src/user-event/__tests__/paste.test.tsx index 702edbb21..9226a6595 100644 --- a/src/user-event/__tests__/paste.test.tsx +++ b/src/user-event/__tests__/paste.test.tsx @@ -130,7 +130,7 @@ describe('paste()', () => { const user = userEvent.setup(); await user.paste(textInput, 'Hi!'); - expect(textInput.props.value).toBe('Hello!'); + expect(textInput).toHaveDisplayValue('Hello!'); }); it('does respect pointer-events prop', async () => { @@ -142,7 +142,7 @@ describe('paste()', () => { const user = userEvent.setup(); await user.paste(textInput, 'Hi!'); - expect(textInput.props.value).toBe('Hello!'); + expect(textInput).toHaveDisplayValue('Hello!'); }); it('supports multiline', async () => { diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts index 42ba54644..9b4ce555f 100644 --- a/src/user-event/clear.ts +++ b/src/user-event/clear.ts @@ -1,7 +1,7 @@ import { ReactTestInstance } from 'react-test-renderer'; import { ErrorWithStack } from '../helpers/errors'; import { isHostTextInput } from '../helpers/host-component-names'; -import { isTextInputEditable } from '../helpers/text-input'; +import { getTextInputValue, isTextInputEditable } from '../helpers/text-input'; import { isPointerEventEnabled } from '../helpers/pointer-events'; import { EventBuilder } from './event-builder'; import { UserEventInstance } from './setup'; @@ -24,7 +24,7 @@ export async function clear(this: UserEventInstance, element: ReactTestInstance) dispatchEvent(element, 'focus', EventBuilder.Common.focus()); // 2. Select all - const textToClear = element.props.value ?? element.props.defaultValue ?? ''; + const textToClear = getTextInputValue(element); const selectionRange = { start: 0, end: textToClear.length, diff --git a/src/user-event/paste.ts b/src/user-event/paste.ts index 856501bfa..7ead1d1f3 100644 --- a/src/user-event/paste.ts +++ b/src/user-event/paste.ts @@ -2,7 +2,7 @@ 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 { getTextInputValue, isTextInputEditable } from '../helpers/text-input'; import { nativeState } from '../native-state'; import { EventBuilder } from './event-builder'; import { UserEventInstance } from './setup'; @@ -28,7 +28,7 @@ export async function paste( dispatchEvent(element, 'focus', EventBuilder.Common.focus()); // 2. Select all - const textToClear = element.props.value ?? element.props.defaultValue ?? ''; + const textToClear = getTextInputValue(element); const rangeToClear = { start: 0, end: textToClear.length }; dispatchEvent(element, 'selectionChange', EventBuilder.TextInput.selectionChange(rangeToClear)); diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts index 53a4befa8..c56e642ae 100644 --- a/src/user-event/setup/setup.ts +++ b/src/user-event/setup/setup.ts @@ -95,7 +95,7 @@ export interface UserEventInstance { * input. * * The exact events sent depend on the props of the TextInput (`editable`, - * `multiline`, value, defaultValue, etc) and passed options. + * `multiline`, etc) and passed options. * * @param element TextInput element to type on * @param text Text to type diff --git a/src/user-event/type/__tests__/type.test.tsx b/src/user-event/type/__tests__/type.test.tsx index 0be7f35fa..8e55c7744 100644 --- a/src/user-event/type/__tests__/type.test.tsx +++ b/src/user-event/type/__tests__/type.test.tsx @@ -374,14 +374,17 @@ describe('type()', () => { }); }); - it('sets native state value for unmanaged text inputs', async () => { + it('unmanaged text inputs preserve their native state', async () => { render(); const user = userEvent.setup(); const input = screen.getByTestId('input'); expect(input).toHaveDisplayValue(''); - await user.type(input, 'abc'); - expect(input).toHaveDisplayValue('abc'); + await user.type(input, 'Hello'); + expect(input).toHaveDisplayValue('Hello'); + + await user.type(input, ' World'); + expect(input).toHaveDisplayValue('Hello World'); }); }); diff --git a/src/user-event/type/type.ts b/src/user-event/type/type.ts index 540a28792..19fa66cc2 100644 --- a/src/user-event/type/type.ts +++ b/src/user-event/type/type.ts @@ -3,7 +3,7 @@ import { isHostTextInput } from '../../helpers/host-component-names'; import { nativeState } from '../../native-state'; import { EventBuilder } from '../event-builder'; import { ErrorWithStack } from '../../helpers/errors'; -import { isTextInputEditable } from '../../helpers/text-input'; +import { getTextInputValue, isTextInputEditable } from '../../helpers/text-input'; import { isPointerEventEnabled } from '../../helpers/pointer-events'; import { UserEventConfig, UserEventInstance } from '../setup'; import { dispatchEvent, wait, getTextContentSize } from '../utils'; @@ -45,9 +45,9 @@ export async function type( dispatchEvent(element, 'pressOut', EventBuilder.Common.touch()); } - let currentText = element.props.value ?? element.props.defaultValue ?? ''; + let currentText = getTextInputValue(element); for (const key of keys) { - const previousText = element.props.value ?? currentText; + const previousText = getTextInputValue(element); const proposedText = applyKey(previousText, key); const isAccepted = isTextChangeAccepted(element, proposedText); currentText = isAccepted ? proposedText : previousText; @@ -60,7 +60,7 @@ export async function type( }); } - const finalText = element.props.value ?? currentText; + const finalText = getTextInputValue(element); await wait(this.config); if (options?.submitEditing) {