Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: toHaveDisplayValue matcher #1463

Merged
merged 8 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';