Skip to content

Commit 344e9b3

Browse files
feat: toHaveDisplayValue matcher (#1463)
* feat: to have display value * add tests for toHaveDisplayValue() * update type of toHaveDisplayValue matcher * rename test file * format * chore: add more tests * refactor: extract common TextInput utils * chore: fix codecov --------- Co-authored-by: Jan Jaworski <[email protected]>
1 parent a7f250d commit 344e9b3

File tree

13 files changed

+207
-24
lines changed

13 files changed

+207
-24
lines changed

src/fireEvent.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import act from './act';
1010
import { isHostElement } from './helpers/component-tree';
1111
import { isHostTextInput } from './helpers/host-component-names';
1212
import { isPointerEventEnabled } from './helpers/pointer-events';
13+
import { isTextInputEditable } from './helpers/text-input';
1314

1415
type EventHandler = (...args: unknown[]) => unknown;
1516

@@ -53,7 +54,7 @@ export function isEventEnabled(
5354
) {
5455
if (isHostTextInput(nearestTouchResponder)) {
5556
return (
56-
nearestTouchResponder?.props.editable !== false ||
57+
isTextInputEditable(nearestTouchResponder) ||
5758
textInputEventsIgnoringEditableProp.has(eventName)
5859
);
5960
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from 'react';
2+
import { View } from 'react-native';
3+
import { render, screen } from '../..';
4+
import { getTextInputValue, isTextInputEditable } from '../text-input';
5+
6+
test('getTextInputValue() throws error when invoked on non-text input', () => {
7+
render(<View testID="view" />);
8+
9+
const view = screen.getByTestId('view');
10+
expect(() => getTextInputValue(view)).toThrowErrorMatchingInlineSnapshot(
11+
`"Element is not a "TextInput", but it has type "View"."`
12+
);
13+
});
14+
15+
test('isTextInputEditable() throws error when invoked on non-text input', () => {
16+
render(<View testID="view" />);
17+
18+
const view = screen.getByTestId('view');
19+
expect(() => isTextInputEditable(view)).toThrowErrorMatchingInlineSnapshot(
20+
`"Element is not a "TextInput", but it has type "View"."`
21+
);
22+
});

src/helpers/text-input.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ReactTestInstance } from 'react-test-renderer';
2+
import { isHostTextInput } from './host-component-names';
3+
4+
export function isTextInputEditable(element: ReactTestInstance) {
5+
if (!isHostTextInput(element)) {
6+
throw new Error(
7+
`Element is not a "TextInput", but it has type "${element.type}".`
8+
);
9+
}
10+
11+
return element.props.editable !== false;
12+
}
13+
14+
export function getTextInputValue(element: ReactTestInstance) {
15+
if (!isHostTextInput(element)) {
16+
throw new Error(
17+
`Element is not a "TextInput", but it has type "${element.type}".`
18+
);
19+
}
20+
21+
return element.props.value ?? element.props.defaultValue;
22+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as React from 'react';
2+
import { TextInput, View } from 'react-native';
3+
import { render, screen } from '../..';
4+
import '../extend-expect';
5+
6+
test('example test', () => {
7+
render(<TextInput testID="text-input" value="test" />);
8+
9+
const textInput = screen.getByTestId('text-input');
10+
expect(textInput).toHaveDisplayValue('test');
11+
});
12+
13+
test('toHaveDisplayValue() on matching display value', () => {
14+
render(<TextInput testID="text-input" value="test" />);
15+
16+
const textInput = screen.getByTestId('text-input');
17+
expect(textInput).toHaveDisplayValue('test');
18+
19+
expect(() => expect(textInput).not.toHaveDisplayValue('test'))
20+
.toThrowErrorMatchingInlineSnapshot(`
21+
"expect(element).not.toHaveDisplayValue()
22+
23+
Expected element not to have display value:
24+
test
25+
Received:
26+
test"
27+
`);
28+
});
29+
30+
test('toHaveDisplayValue() on non-matching display value', () => {
31+
render(<TextInput testID="text-input" value="test" />);
32+
33+
const textInput = screen.getByTestId('text-input');
34+
expect(textInput).not.toHaveDisplayValue('non-test');
35+
36+
expect(() => expect(textInput).toHaveDisplayValue('non-test'))
37+
.toThrowErrorMatchingInlineSnapshot(`
38+
"expect(element).toHaveDisplayValue()
39+
40+
Expected element to have display value:
41+
non-test
42+
Received:
43+
test"
44+
`);
45+
});
46+
47+
test("toHaveDisplayValue() on non-'TextInput' elements", () => {
48+
render(<View testID="view" />);
49+
50+
const view = screen.getByTestId('view');
51+
expect(() =>
52+
expect(view).toHaveDisplayValue('test')
53+
).toThrowErrorMatchingInlineSnapshot(
54+
`"toHaveDisplayValue() works only with host "TextInput" elements. Passed element has type "View"."`
55+
);
56+
});
57+
58+
test('toHaveDisplayValue() performing partial match', () => {
59+
render(<TextInput testID="text-input" value="Hello World" />);
60+
61+
const textInput = screen.getByTestId('text-input');
62+
expect(textInput).toHaveDisplayValue('Hello World');
63+
64+
expect(textInput).not.toHaveDisplayValue('hello world');
65+
expect(textInput).not.toHaveDisplayValue('Hello');
66+
expect(textInput).not.toHaveDisplayValue('World');
67+
68+
expect(textInput).toHaveDisplayValue('Hello World', { exact: false });
69+
expect(textInput).toHaveDisplayValue('hello', { exact: false });
70+
expect(textInput).toHaveDisplayValue('world', { exact: false });
71+
});
72+
73+
test('toHaveDisplayValue() uses defaultValue', () => {
74+
render(<TextInput testID="text-input" defaultValue="default" />);
75+
76+
const textInput = screen.getByTestId('text-input');
77+
expect(textInput).toHaveDisplayValue('default');
78+
});
79+
80+
test('toHaveDisplayValue() prioritizes value over defaultValue', () => {
81+
render(
82+
<TextInput testID="text-input" value="value" defaultValue="default" />
83+
);
84+
85+
const textInput = screen.getByTestId('text-input');
86+
expect(textInput).toHaveDisplayValue('value');
87+
});

src/matchers/extend-expect.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { TextMatch, TextMatchOptions } from '../matches';
2+
13
export interface JestNativeMatchers<R> {
24
toBeOnTheScreen(): R;
35
toBeEmptyElement(): R;
6+
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
47
}
58

69
// Implicit Jest global `expect`.

src/matchers/extend-expect.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import { toBeOnTheScreen } from './to-be-on-the-screen';
44
import { toBeEmptyElement } from './to-be-empty-element';
5+
import { toHaveDisplayValue } from './to-have-display-value';
56

67
expect.extend({
78
toBeOnTheScreen,
89
toBeEmptyElement,
10+
toHaveDisplayValue,
911
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { ReactTestInstance } from 'react-test-renderer';
2+
import { matcherHint } from 'jest-matcher-utils';
3+
import { isHostTextInput } from '../helpers/host-component-names';
4+
import { ErrorWithStack } from '../helpers/errors';
5+
import { getTextInputValue } from '../helpers/text-input';
6+
import { TextMatch, TextMatchOptions, matches } from '../matches';
7+
import { checkHostElement, formatMessage } from './utils';
8+
9+
export function toHaveDisplayValue(
10+
this: jest.MatcherContext,
11+
element: ReactTestInstance,
12+
expectedValue: TextMatch,
13+
options?: TextMatchOptions
14+
) {
15+
checkHostElement(element, toHaveDisplayValue, this);
16+
17+
if (!isHostTextInput(element)) {
18+
throw new ErrorWithStack(
19+
`toHaveDisplayValue() works only with host "TextInput" elements. Passed element has type "${element.type}".`,
20+
toHaveDisplayValue
21+
);
22+
}
23+
24+
const receivedValue = getTextInputValue(element);
25+
26+
return {
27+
pass: matches(
28+
expectedValue,
29+
receivedValue,
30+
options?.normalizer,
31+
options?.exact
32+
),
33+
message: () => {
34+
return [
35+
formatMessage(
36+
matcherHint(
37+
`${this.isNot ? '.not' : ''}.toHaveDisplayValue`,
38+
'element',
39+
''
40+
),
41+
`Expected element ${this.isNot ? 'not to' : 'to'} have display value`,
42+
expectedValue,
43+
'Received',
44+
receivedValue
45+
),
46+
].join('\n');
47+
},
48+
};
49+
}

src/queries/displayValue.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { findAll } from '../helpers/findAll';
33
import { isHostTextInput } from '../helpers/host-component-names';
4+
import { getTextInputValue } from '../helpers/text-input';
45
import { matches, TextMatch, TextMatchOptions } from '../matches';
56
import { makeQueries } from './makeQueries';
67
import type {
@@ -17,13 +18,12 @@ type ByDisplayValueOptions = CommonQueryOptions & TextMatchOptions;
1718

1819
const matchDisplayValue = (
1920
node: ReactTestInstance,
20-
value: TextMatch,
21+
expectedValue: TextMatch,
2122
options: TextMatchOptions = {}
2223
) => {
2324
const { exact, normalizer } = options;
24-
const nodeValue = node.props.value ?? node.props.defaultValue;
25-
26-
return matches(value, nodeValue, normalizer, exact);
25+
const nodeValue = getTextInputValue(node);
26+
return matches(expectedValue, nodeValue, normalizer, exact);
2727
};
2828

2929
const queryAllByDisplayValue = (

src/user-event/clear.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { ErrorWithStack } from '../helpers/errors';
33
import { isHostTextInput } from '../helpers/host-component-names';
4+
import { isTextInputEditable } from '../helpers/text-input';
45
import { isPointerEventEnabled } from '../helpers/pointer-events';
56
import { EventBuilder } from './event-builder';
67
import { UserEventInstance } from './setup';
7-
import { dispatchEvent, wait, isEditableTextInput } from './utils';
8+
import { dispatchEvent, wait } from './utils';
89
import { emitTypingEvents } from './type/type';
910

1011
export async function clear(
@@ -18,7 +19,7 @@ export async function clear(
1819
);
1920
}
2021

21-
if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
22+
if (!isTextInputEditable(element) || !isPointerEventEnabled(element)) {
2223
return;
2324
}
2425

src/user-event/press/press.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import act from '../../act';
33
import { getHostParent } from '../../helpers/component-tree';
4+
import { isTextInputEditable } from '../../helpers/text-input';
45
import { isPointerEventEnabled } from '../../helpers/pointer-events';
5-
import { isHostText } from '../../helpers/host-component-names';
6+
import {
7+
isHostText,
8+
isHostTextInput,
9+
} from '../../helpers/host-component-names';
610
import { EventBuilder } from '../event-builder';
711
import { UserEventConfig, UserEventInstance } from '../setup';
8-
import {
9-
dispatchEvent,
10-
isEditableTextInput,
11-
wait,
12-
warnAboutRealTimersIfNeeded,
13-
} from '../utils';
12+
import { dispatchEvent, wait, warnAboutRealTimersIfNeeded } from '../utils';
1413
import { DEFAULT_MIN_PRESS_DURATION } from './constants';
1514

1615
export interface PressOptions {
@@ -53,7 +52,11 @@ const basePress = async (
5352
return;
5453
}
5554

56-
if (isEditableTextInput(element) && isPointerEventEnabled(element)) {
55+
if (
56+
isHostTextInput(element) &&
57+
isTextInputEditable(element) &&
58+
isPointerEventEnabled(element)
59+
) {
5760
await emitTextInputPressEvents(config, element, options);
5861
return;
5962
}

0 commit comments

Comments
 (0)