Skip to content

Commit

Permalink
fix: use native state across the queries and matchers (#1667)
Browse files Browse the repository at this point in the history
* fix: improve text input value getting in type, clear, paste

* chore: migrate other use-cases to use `getTextInputValue`

* chore: add relevant tests

* refactor: self code review

* refactor: self code review
  • Loading branch information
mdjastrzebski authored Sep 12, 2024
1 parent 8390011 commit 7ba47b8
Show file tree
Hide file tree
Showing 10 changed files with 37 additions and 25 deletions.
4 changes: 2 additions & 2 deletions src/fire-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion src/matchers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export interface JestNativeMatchers<R> {
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)
Expand Down
21 changes: 15 additions & 6 deletions src/queries/__tests__/display-value.test.tsx
Original file line number Diff line number Diff line change
@@ -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?';
Expand Down Expand Up @@ -30,13 +30,12 @@ const Banana = () => (

test('getByDisplayValue, queryByDisplayValue', () => {
render(<Banana />);
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',
);
Expand Down Expand Up @@ -203,3 +202,13 @@ test('error message renders the element tree, preserving only helpful props', as
/>"
`);
});

test('supports unmanaged TextInput element', () => {
render(<TextInput testID="input" />);

const input = screen.getByDisplayValue('');
expect(input).toHaveDisplayValue('');

fireEvent.changeText(input, 'Hello!');
expect(input).toHaveDisplayValue('Hello!');
});
4 changes: 2 additions & 2 deletions src/user-event/__tests__/clear.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
4 changes: 2 additions & 2 deletions src/user-event/__tests__/paste.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
4 changes: 2 additions & 2 deletions src/user-event/clear.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/user-event/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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));

Expand Down
2 changes: 1 addition & 1 deletion src/user-event/setup/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions src/user-event/type/__tests__/type.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<TextInput testID="input" />);

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

0 comments on commit 7ba47b8

Please sign in to comment.