diff --git a/src/fireEvent.ts b/src/fireEvent.ts
index 68746e300..a27be7c78 100644
--- a/src/fireEvent.ts
+++ b/src/fireEvent.ts
@@ -10,6 +10,7 @@ import act from './act';
import { isHostElement } from './helpers/component-tree';
import { isHostTextInput } from './helpers/host-component-names';
import { isPointerEventEnabled } from './helpers/pointer-events';
+import { isTextInputEditable } from './helpers/text-input';
type EventHandler = (...args: unknown[]) => unknown;
@@ -53,7 +54,7 @@ export function isEventEnabled(
) {
if (isHostTextInput(nearestTouchResponder)) {
return (
- nearestTouchResponder?.props.editable !== false ||
+ isTextInputEditable(nearestTouchResponder) ||
textInputEventsIgnoringEditableProp.has(eventName)
);
}
diff --git a/src/helpers/__tests__/text-input.test.tsx b/src/helpers/__tests__/text-input.test.tsx
new file mode 100644
index 000000000..88606b722
--- /dev/null
+++ b/src/helpers/__tests__/text-input.test.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react';
+import { View } from 'react-native';
+import { render, screen } from '../..';
+import { getTextInputValue, isTextInputEditable } from '../text-input';
+
+test('getTextInputValue() throws error when invoked on non-text input', () => {
+ render();
+
+ const view = screen.getByTestId('view');
+ expect(() => getTextInputValue(view)).toThrowErrorMatchingInlineSnapshot(
+ `"Element is not a "TextInput", but it has type "View"."`
+ );
+});
+
+test('isTextInputEditable() throws error when invoked on non-text input', () => {
+ render();
+
+ const view = screen.getByTestId('view');
+ expect(() => isTextInputEditable(view)).toThrowErrorMatchingInlineSnapshot(
+ `"Element is not a "TextInput", but it has type "View"."`
+ );
+});
diff --git a/src/helpers/text-input.ts b/src/helpers/text-input.ts
new file mode 100644
index 000000000..de0af158b
--- /dev/null
+++ b/src/helpers/text-input.ts
@@ -0,0 +1,22 @@
+import { ReactTestInstance } from 'react-test-renderer';
+import { isHostTextInput } from './host-component-names';
+
+export function isTextInputEditable(element: ReactTestInstance) {
+ if (!isHostTextInput(element)) {
+ throw new Error(
+ `Element is not a "TextInput", but it has type "${element.type}".`
+ );
+ }
+
+ return element.props.editable !== false;
+}
+
+export function getTextInputValue(element: ReactTestInstance) {
+ if (!isHostTextInput(element)) {
+ throw new Error(
+ `Element is not a "TextInput", but it has type "${element.type}".`
+ );
+ }
+
+ return element.props.value ?? element.props.defaultValue;
+}
diff --git a/src/matchers/__tests__/to-have-display-value.test.tsx b/src/matchers/__tests__/to-have-display-value.test.tsx
new file mode 100644
index 000000000..b523f81db
--- /dev/null
+++ b/src/matchers/__tests__/to-have-display-value.test.tsx
@@ -0,0 +1,87 @@
+import * as React from 'react';
+import { TextInput, View } from 'react-native';
+import { render, screen } from '../..';
+import '../extend-expect';
+
+test('example test', () => {
+ render();
+
+ const textInput = screen.getByTestId('text-input');
+ expect(textInput).toHaveDisplayValue('test');
+});
+
+test('toHaveDisplayValue() on matching display value', () => {
+ render();
+
+ const textInput = screen.getByTestId('text-input');
+ expect(textInput).toHaveDisplayValue('test');
+
+ expect(() => expect(textInput).not.toHaveDisplayValue('test'))
+ .toThrowErrorMatchingInlineSnapshot(`
+ "expect(element).not.toHaveDisplayValue()
+
+ Expected element not to have display value:
+ test
+ Received:
+ test"
+ `);
+});
+
+test('toHaveDisplayValue() on non-matching display value', () => {
+ render();
+
+ const textInput = screen.getByTestId('text-input');
+ expect(textInput).not.toHaveDisplayValue('non-test');
+
+ expect(() => expect(textInput).toHaveDisplayValue('non-test'))
+ .toThrowErrorMatchingInlineSnapshot(`
+ "expect(element).toHaveDisplayValue()
+
+ Expected element to have display value:
+ non-test
+ Received:
+ test"
+ `);
+});
+
+test("toHaveDisplayValue() on non-'TextInput' elements", () => {
+ render();
+
+ const view = screen.getByTestId('view');
+ expect(() =>
+ expect(view).toHaveDisplayValue('test')
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"toHaveDisplayValue() works only with host "TextInput" elements. Passed element has type "View"."`
+ );
+});
+
+test('toHaveDisplayValue() performing partial match', () => {
+ render();
+
+ const textInput = screen.getByTestId('text-input');
+ expect(textInput).toHaveDisplayValue('Hello World');
+
+ expect(textInput).not.toHaveDisplayValue('hello world');
+ expect(textInput).not.toHaveDisplayValue('Hello');
+ expect(textInput).not.toHaveDisplayValue('World');
+
+ expect(textInput).toHaveDisplayValue('Hello World', { exact: false });
+ expect(textInput).toHaveDisplayValue('hello', { exact: false });
+ expect(textInput).toHaveDisplayValue('world', { exact: false });
+});
+
+test('toHaveDisplayValue() uses defaultValue', () => {
+ render();
+
+ const textInput = screen.getByTestId('text-input');
+ expect(textInput).toHaveDisplayValue('default');
+});
+
+test('toHaveDisplayValue() prioritizes value over defaultValue', () => {
+ render(
+
+ );
+
+ const textInput = screen.getByTestId('text-input');
+ expect(textInput).toHaveDisplayValue('value');
+});
diff --git a/src/matchers/extend-expect.d.ts b/src/matchers/extend-expect.d.ts
index 435d4c509..16a31109e 100644
--- a/src/matchers/extend-expect.d.ts
+++ b/src/matchers/extend-expect.d.ts
@@ -1,6 +1,9 @@
+import { TextMatch, TextMatchOptions } from '../matches';
+
export interface JestNativeMatchers {
toBeOnTheScreen(): R;
toBeEmptyElement(): R;
+ toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
}
// Implicit Jest global `expect`.
diff --git a/src/matchers/extend-expect.ts b/src/matchers/extend-expect.ts
index dc7744189..bf6b1bac5 100644
--- a/src/matchers/extend-expect.ts
+++ b/src/matchers/extend-expect.ts
@@ -2,8 +2,10 @@
import { toBeOnTheScreen } from './to-be-on-the-screen';
import { toBeEmptyElement } from './to-be-empty-element';
+import { toHaveDisplayValue } from './to-have-display-value';
expect.extend({
toBeOnTheScreen,
toBeEmptyElement,
+ toHaveDisplayValue,
});
diff --git a/src/matchers/to-have-display-value.tsx b/src/matchers/to-have-display-value.tsx
new file mode 100644
index 000000000..8813f2273
--- /dev/null
+++ b/src/matchers/to-have-display-value.tsx
@@ -0,0 +1,49 @@
+import type { ReactTestInstance } from 'react-test-renderer';
+import { matcherHint } from 'jest-matcher-utils';
+import { isHostTextInput } from '../helpers/host-component-names';
+import { ErrorWithStack } from '../helpers/errors';
+import { getTextInputValue } from '../helpers/text-input';
+import { TextMatch, TextMatchOptions, matches } from '../matches';
+import { checkHostElement, formatMessage } from './utils';
+
+export function toHaveDisplayValue(
+ this: jest.MatcherContext,
+ element: ReactTestInstance,
+ expectedValue: TextMatch,
+ options?: TextMatchOptions
+) {
+ checkHostElement(element, toHaveDisplayValue, this);
+
+ if (!isHostTextInput(element)) {
+ throw new ErrorWithStack(
+ `toHaveDisplayValue() works only with host "TextInput" elements. Passed element has type "${element.type}".`,
+ toHaveDisplayValue
+ );
+ }
+
+ const receivedValue = getTextInputValue(element);
+
+ return {
+ pass: matches(
+ expectedValue,
+ receivedValue,
+ options?.normalizer,
+ options?.exact
+ ),
+ message: () => {
+ return [
+ formatMessage(
+ matcherHint(
+ `${this.isNot ? '.not' : ''}.toHaveDisplayValue`,
+ 'element',
+ ''
+ ),
+ `Expected element ${this.isNot ? 'not to' : 'to'} have display value`,
+ expectedValue,
+ 'Received',
+ receivedValue
+ ),
+ ].join('\n');
+ },
+ };
+}
diff --git a/src/queries/displayValue.ts b/src/queries/displayValue.ts
index d521dbf36..0d43e4c7c 100644
--- a/src/queries/displayValue.ts
+++ b/src/queries/displayValue.ts
@@ -1,6 +1,7 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { findAll } from '../helpers/findAll';
import { isHostTextInput } from '../helpers/host-component-names';
+import { getTextInputValue } from '../helpers/text-input';
import { matches, TextMatch, TextMatchOptions } from '../matches';
import { makeQueries } from './makeQueries';
import type {
@@ -17,13 +18,12 @@ type ByDisplayValueOptions = CommonQueryOptions & TextMatchOptions;
const matchDisplayValue = (
node: ReactTestInstance,
- value: TextMatch,
+ expectedValue: TextMatch,
options: TextMatchOptions = {}
) => {
const { exact, normalizer } = options;
- const nodeValue = node.props.value ?? node.props.defaultValue;
-
- return matches(value, nodeValue, normalizer, exact);
+ const nodeValue = getTextInputValue(node);
+ return matches(expectedValue, nodeValue, normalizer, exact);
};
const queryAllByDisplayValue = (
diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts
index 46df2a22d..a60013b08 100644
--- a/src/user-event/clear.ts
+++ b/src/user-event/clear.ts
@@ -1,10 +1,11 @@
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 { isPointerEventEnabled } from '../helpers/pointer-events';
import { EventBuilder } from './event-builder';
import { UserEventInstance } from './setup';
-import { dispatchEvent, wait, isEditableTextInput } from './utils';
+import { dispatchEvent, wait } from './utils';
import { emitTypingEvents } from './type/type';
export async function clear(
@@ -18,7 +19,7 @@ export async function clear(
);
}
- if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
+ if (!isTextInputEditable(element) || !isPointerEventEnabled(element)) {
return;
}
diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts
index 7c4835cf7..18e7fef47 100644
--- a/src/user-event/press/press.ts
+++ b/src/user-event/press/press.ts
@@ -1,16 +1,15 @@
import { ReactTestInstance } from 'react-test-renderer';
import act from '../../act';
import { getHostParent } from '../../helpers/component-tree';
+import { isTextInputEditable } from '../../helpers/text-input';
import { isPointerEventEnabled } from '../../helpers/pointer-events';
-import { isHostText } from '../../helpers/host-component-names';
+import {
+ isHostText,
+ isHostTextInput,
+} from '../../helpers/host-component-names';
import { EventBuilder } from '../event-builder';
import { UserEventConfig, UserEventInstance } from '../setup';
-import {
- dispatchEvent,
- isEditableTextInput,
- wait,
- warnAboutRealTimersIfNeeded,
-} from '../utils';
+import { dispatchEvent, wait, warnAboutRealTimersIfNeeded } from '../utils';
import { DEFAULT_MIN_PRESS_DURATION } from './constants';
export interface PressOptions {
@@ -53,7 +52,11 @@ const basePress = async (
return;
}
- if (isEditableTextInput(element) && isPointerEventEnabled(element)) {
+ if (
+ isHostTextInput(element) &&
+ isTextInputEditable(element) &&
+ isPointerEventEnabled(element)
+ ) {
await emitTextInputPressEvents(config, element, options);
return;
}
diff --git a/src/user-event/type/type.ts b/src/user-event/type/type.ts
index 6ea309376..13cc5448a 100644
--- a/src/user-event/type/type.ts
+++ b/src/user-event/type/type.ts
@@ -2,10 +2,10 @@ import { ReactTestInstance } from 'react-test-renderer';
import { isHostTextInput } from '../../helpers/host-component-names';
import { EventBuilder } from '../event-builder';
import { ErrorWithStack } from '../../helpers/errors';
+import { isTextInputEditable } from '../../helpers/text-input';
import { isPointerEventEnabled } from '../../helpers/pointer-events';
import { UserEventConfig, UserEventInstance } from '../setup';
import { dispatchEvent, wait, getTextContentSize } from '../utils';
-
import { parseKeys } from './parseKeys';
export interface TypeOptions {
@@ -27,7 +27,7 @@ export async function type(
}
// Skip events if the element is disabled
- if (element.props.editable === false || !isPointerEventEnabled(element)) {
+ if (!isTextInputEditable(element) || !isPointerEventEnabled(element)) {
return;
}
diff --git a/src/user-event/utils/host-components.ts b/src/user-event/utils/host-components.ts
deleted file mode 100644
index 1a43785be..000000000
--- a/src/user-event/utils/host-components.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-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 d97431dae..56e00613b 100644
--- a/src/user-event/utils/index.ts
+++ b/src/user-event/utils/index.ts
@@ -1,6 +1,5 @@
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';