Skip to content

Commit

Permalink
feat: toHaveDisplayValue matcher (#1463)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
mdjastrzebski and jaworek authored Aug 24, 2023
1 parent a7f250d commit 344e9b3
Show file tree
Hide file tree
Showing 13 changed files with 207 additions and 24 deletions.
3 changes: 2 additions & 1 deletion src/fireEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -53,7 +54,7 @@ export function isEventEnabled(
) {
if (isHostTextInput(nearestTouchResponder)) {
return (
nearestTouchResponder?.props.editable !== false ||
isTextInputEditable(nearestTouchResponder) ||
textInputEventsIgnoringEditableProp.has(eventName)
);
}
Expand Down
22 changes: 22 additions & 0 deletions src/helpers/__tests__/text-input.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<View testID="view" />);

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(<View testID="view" />);

const view = screen.getByTestId('view');
expect(() => isTextInputEditable(view)).toThrowErrorMatchingInlineSnapshot(
`"Element is not a "TextInput", but it has type "View"."`
);
});
22 changes: 22 additions & 0 deletions src/helpers/text-input.ts
Original file line number Diff line number Diff line change
@@ -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;
}
87 changes: 87 additions & 0 deletions src/matchers/__tests__/to-have-display-value.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<TextInput testID="text-input" value="test" />);

const textInput = screen.getByTestId('text-input');
expect(textInput).toHaveDisplayValue('test');
});

test('toHaveDisplayValue() on matching display value', () => {
render(<TextInput testID="text-input" value="test" />);

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(<TextInput testID="text-input" value="test" />);

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(<View testID="view" />);

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(<TextInput testID="text-input" value="Hello World" />);

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(<TextInput testID="text-input" defaultValue="default" />);

const textInput = screen.getByTestId('text-input');
expect(textInput).toHaveDisplayValue('default');
});

test('toHaveDisplayValue() prioritizes value over defaultValue', () => {
render(
<TextInput testID="text-input" value="value" defaultValue="default" />
);

const textInput = screen.getByTestId('text-input');
expect(textInput).toHaveDisplayValue('value');
});
3 changes: 3 additions & 0 deletions src/matchers/extend-expect.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { TextMatch, TextMatchOptions } from '../matches';

export interface JestNativeMatchers<R> {
toBeOnTheScreen(): R;
toBeEmptyElement(): R;
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
}

// Implicit Jest global `expect`.
Expand Down
2 changes: 2 additions & 0 deletions src/matchers/extend-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
49 changes: 49 additions & 0 deletions src/matchers/to-have-display-value.tsx
Original file line number Diff line number Diff line change
@@ -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');
},
};
}
8 changes: 4 additions & 4 deletions src/queries/displayValue.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 = (
Expand Down
5 changes: 3 additions & 2 deletions src/user-event/clear.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -18,7 +19,7 @@ export async function clear(
);
}

if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
if (!isTextInputEditable(element) || !isPointerEventEnabled(element)) {
return;
}

Expand Down
19 changes: 11 additions & 8 deletions src/user-event/press/press.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/user-event/type/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}

Expand Down
6 changes: 0 additions & 6 deletions src/user-event/utils/host-components.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/user-event/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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';

0 comments on commit 344e9b3

Please sign in to comment.