Skip to content

feat(breaking): Return host component for all queries #1234

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
5b214bb
BREAKING CHANGE : make placeholdertext queries return host elements
Nov 20, 2022
88c7f93
BREAKING CHANGE : make displayValue queries return host elements
Nov 20, 2022
0b229b6
BREAKING CHANGE: make text queries return host elements
Nov 20, 2022
5624984
refactor: reset config to default before each test
Nov 20, 2022
6bbf105
feat: use component names from config and add api to configure them
Nov 20, 2022
80e1139
feat: remove legacy option from queries
pierrezimmermannbam Nov 28, 2022
8ee6995
refactor: reorganize legacy and breaking tests for queries
pierrezimmermannbam Nov 28, 2022
bdcbdf2
feat: change error messages for mismatching host component names
pierrezimmermannbam Nov 28, 2022
f34d77d
fix: do not update screen when running auto detection
pierrezimmermannbam Nov 28, 2022
5795c41
fix: typo in comment
pierrezimmermannbam Nov 28, 2022
6648c5c
refactor: use an object for matchTextContent params
pierrezimmermannbam Nov 28, 2022
6314f59
tests: rewrite some tests to match repo patterns
pierrezimmermannbam Nov 28, 2022
34eb57a
fix: use legacy queries by default to prevent breaking changes
pierrezimmermannbam Nov 28, 2022
d09a738
fix: remove wrong uppercase in flow typings
pierrezimmermannbam Nov 28, 2022
fd5c342
docs: remove section on useLegacy queries as it wont be a public api
pierrezimmermannbam Nov 28, 2022
17e42a6
docs: small improvements based on review
pierrezimmermannbam Nov 28, 2022
568981e
fix: remove typo in error message
Dec 4, 2022
46c70a4
refactor: rename getReactNativeHostComponentNames to detectHostCompon…
Dec 4, 2022
eb30f96
refactor: use useBreakingChange from internal config instead of useLe…
Dec 4, 2022
2245bd8
fix: use fake timers in test so it doesnt take too much time
Dec 4, 2022
c19a7db
refactor: replace type assertions by real type checking
Dec 4, 2022
78724f3
fix: revert change due to rebase
Dec 5, 2022
e911932
fix: improvements after review
Dec 5, 2022
f627ad4
refactor: define type to match inside matchTextContent function
Dec 9, 2022
9eb4dc4
refactor: rename function to get error message
Dec 9, 2022
57b704e
feat: display error when auto detect of host component names throw
Dec 9, 2022
751af46
fix: remove unused ts-expect-error after ts version bump
Jan 6, 2023
366f08f
refactor: run auto detection on render if it hasnt been run yet
Jan 8, 2023
e83c806
refactor: reuse type imports that had been converted to regular ones
Jan 9, 2023
8377660
refactor: add comments to distinguish breaking and legacy
Jan 9, 2023
3c8fd8c
refactor: move config logic into detectComponentHostNames function
Jan 9, 2023
b454746
refactor: rename function to detectComponentHostNamesIfNeeded
Jan 9, 2023
8145557
tests: add test on detectHostComponentNamesIfNeeded
Jan 9, 2023
6f1fdd0
fix: use host component name from config for textinput in fireEvent
Jan 11, 2023
097465c
refactor: run autodetection in text, displayvalue and placeholder que…
Jan 11, 2023
0568bc0
refactor: remove useless async declaration
Jan 11, 2023
820d164
refactor: improvements for readability from review
Jan 11, 2023
ceed10e
Update src/__tests__/host-component-names.test.tsx
mdjastrzebski Jan 11, 2023
6b18bbe
refactor: declare variable for host component names in detection func…
Jan 15, 2023
a7ae9e1
refactor: rename detectHostComponentNamesIfNeeded to getHostComponent…
Jan 15, 2023
b25abeb
feat: use getHostComponentNames in fireEvent for textinput host name
Jan 15, 2023
80788c2
refactor: revert refactor on matchTextContent
Jan 15, 2023
def1cc4
refactor: add comment to indicate impossible path
Jan 15, 2023
7557fa3
chore: simplify jest-setup files
mdjastrzebski Jan 15, 2023
880b7e8
refactor: code review changes
mdjastrzebski Jan 15, 2023
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
7 changes: 7 additions & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { resetToDefaults } from './src/pure';

jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');

beforeEach(() => {
resetToDefaults();
});
1 change: 0 additions & 1 deletion jestSetup.js

This file was deleted.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,7 @@
},
"jest": {
"preset": "./jest-preset",
"setupFiles": [
"./jestSetup.js"
],
"setupFilesAfterEnv": ["./jest-setup.ts"],
"testPathIgnorePatterns": [
"timerUtils",
"examples/"
Expand Down
4 changes: 0 additions & 4 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import {
configureInternal,
} from '../config';

beforeEach(() => {
resetToDefaults();
});

test('getConfig() returns existing configuration', () => {
expect(getConfig().useBreakingChanges).toEqual(false);
expect(getConfig().asyncUtilTimeout).toEqual(1000);
Expand Down
48 changes: 48 additions & 0 deletions src/__tests__/host-component-names.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { View } from 'react-native';
import TestRenderer from 'react-test-renderer';
import { configureInternal, getConfig } from '../config';
import { getHostComponentNames } from '../helpers/host-component-names';

const mockCreate = jest.spyOn(TestRenderer, 'create') as jest.Mock;

describe('getHostComponentNames', () => {
test('updates internal config with host component names when they are not defined', () => {
expect(getConfig().hostComponentNames).toBeUndefined();

getHostComponentNames();

expect(getConfig().hostComponentNames).toEqual({
text: 'Text',
textInput: 'TextInput',
});
});

test('does not update internal config when host component names are already configured', () => {
configureInternal({
hostComponentNames: { text: 'banana', textInput: 'banana' },
});

getHostComponentNames();

expect(getConfig().hostComponentNames).toEqual({
text: 'banana',
textInput: 'banana',
});
});

test('throw an error when autodetection fails', () => {
mockCreate.mockReturnValue({
root: { type: View, children: [], props: {} },
});

expect(() => getHostComponentNames()).toThrowErrorMatchingInlineSnapshot(`
"Trying to detect host component names triggered the following error:

Unable to find an element with testID: text

There seems to be an issue with your configuration that prevents React Native Testing Library from working correctly.
Please check if you are using compatible versions of React Native and React Native Testing Library.
"
`);
});
});
3 changes: 1 addition & 2 deletions src/__tests__/render-debug.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react';
import { View, Text, TextInput, Pressable } from 'react-native';
import stripAnsi from 'strip-ansi';
import { render, fireEvent, resetToDefaults, configure } from '..';
import { render, fireEvent, configure } from '..';

type ConsoleLogMock = jest.Mock<Array<string>>;

Expand All @@ -18,7 +18,6 @@ const ignoreWarnings = ['Using debug("message") is deprecated'];
const realConsoleWarn = console.warn;

beforeEach(() => {
resetToDefaults();
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation((message) => {
if (!ignoreWarnings.some((warning) => message.includes(warning))) {
Expand Down
6 changes: 1 addition & 5 deletions src/__tests__/render.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import * as React from 'react';
import { View, Text, TextInput, Pressable, SafeAreaView } from 'react-native';
import { render, fireEvent, RenderAPI, resetToDefaults } from '..';
import { render, fireEvent, RenderAPI } from '..';

const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
Expand All @@ -10,10 +10,6 @@ const INPUT_CHEF = 'I inspected freshie';
const DEFAULT_INPUT_CHEF = 'What did you inspect?';
const DEFAULT_INPUT_CUSTOMER = 'What banana?';

beforeEach(() => {
resetToDefaults();
});

class MyButton extends React.Component<any> {
render() {
return (
Expand Down
6 changes: 1 addition & 5 deletions src/__tests__/waitFor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Text, TouchableOpacity, View, Pressable } from 'react-native';
import { fireEvent, render, waitFor, configure, resetToDefaults } from '..';
import { fireEvent, render, waitFor, configure } from '..';

class Banana extends React.Component<any> {
changeFresh = () => {
Expand All @@ -19,10 +19,6 @@ class Banana extends React.Component<any> {
}
}

beforeEach(() => {
resetToDefaults();
});

class BananaContainer extends React.Component<{}, any> {
state = { fresh: false };

Expand Down
9 changes: 9 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DebugOptions } from './helpers/debugDeep';
/**
* Global configuration options for React Native Testing Library.
*/

export type Config = {
/** Default timeout, in ms, for `waitFor` and `findBy*` queries. */
asyncUtilTimeout: number;
Expand All @@ -19,9 +20,17 @@ export type ConfigAliasOptions = {
defaultHidden: boolean;
};

export type HostComponentNames = {
text: string;
textInput: string;
};

export type InternalConfig = Config & {
/** Whether to use breaking changes intended for next major version release. */
useBreakingChanges: boolean;

/** Names for key React Native host components. */
hostComponentNames?: HostComponentNames;
};

const defaultConfig: InternalConfig = {
Expand Down
6 changes: 3 additions & 3 deletions src/fireEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TextInput } from 'react-native';
import act from './act';
import { isHostElement } from './helpers/component-tree';
import { filterNodeByType } from './helpers/filterNodeByType';
import { getHostComponentNames } from './helpers/host-component-names';

type EventHandler = (...args: any) => unknown;

Expand All @@ -14,12 +15,11 @@ const isTextInput = (element?: ReactTestInstance) => {
// We have to test if the element type is either the TextInput component
// (which would if it is a composite component) or the string
// TextInput (which would be true if it is a host component)
// All queries but the one by testID return composite component and event
// if all queries returned host components, since fireEvent bubbles up
// All queries return host components but since fireEvent bubbles up
// it would trigger the parent prop without the composite component check
return (
filterNodeByType(element, TextInput) ||
filterNodeByType(element, 'TextInput')
filterNodeByType(element, getHostComponentNames().textInput)
);
};

Expand Down
50 changes: 50 additions & 0 deletions src/helpers/host-component-names.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { Text, TextInput, View } from 'react-native';
import TestRenderer from 'react-test-renderer';
import { configureInternal, getConfig, HostComponentNames } from '../config';
import { getQueriesForElement } from '../within';

const defaultErrorMessage = `There seems to be an issue with your configuration that prevents React Native Testing Library from working correctly.
Please check if you are using compatible versions of React Native and React Native Testing Library.`;

export function getHostComponentNames(): HostComponentNames {
const configHostComponentNames = getConfig().hostComponentNames;
if (configHostComponentNames) {
return configHostComponentNames;
}

try {
const renderer = TestRenderer.create(
<View>
<Text testID="text">Hello</Text>
<TextInput testID="textInput" />
</View>
);
const { getByTestId } = getQueriesForElement(renderer.root);
const textHostName = getByTestId('text').type;
const textInputHostName = getByTestId('textInput').type;

// This code path should not happen as getByTestId always returns host elements.
if (
typeof textHostName !== 'string' ||
typeof textInputHostName !== 'string'
) {
throw new Error(defaultErrorMessage);
}

const hostComponentNames = {
text: textHostName,
textInput: textInputHostName,
};
configureInternal({ hostComponentNames });
return hostComponentNames;
} catch (error) {
const errorMessage =
error && typeof error === 'object' && 'message' in error
? error.message
: null;

throw new Error(`Trying to detect host component names triggered the following error:\n\n${errorMessage}\n\n${defaultErrorMessage}
`);
}
}
7 changes: 6 additions & 1 deletion src/helpers/matchers/matchTextContent.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { Text } from 'react-native';
import type { ReactTestInstance } from 'react-test-renderer';
import { getConfig } from '../../config';
import { matches, TextMatch, TextMatchOptions } from '../../matches';
import { filterNodeByType } from '../filterNodeByType';
import { getTextContent } from '../getTextContent';
import { getHostComponentNames } from '../host-component-names';

export function matchTextContent(
node: ReactTestInstance,
text: TextMatch,
options: TextMatchOptions = {}
) {
if (!filterNodeByType(node, Text)) {
const textType = getConfig().useBreakingChanges
? getHostComponentNames().text
: Text;
if (!filterNodeByType(node, textType)) {
return false;
}

Expand Down
136 changes: 136 additions & 0 deletions src/queries/__tests__/displayValue.breaking.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as React from 'react';
import { View, TextInput } from 'react-native';

import { render } from '../..';
import { configureInternal } from '../../config';

const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
const INPUT_FRESHNESS = 'Custom Freshie';
const INPUT_CHEF = 'I inspected freshie';
const DEFAULT_INPUT_CHEF = 'What did you inspect?';
const DEFAULT_INPUT_CUSTOMER = 'What banana?';

beforeEach(() => configureInternal({ useBreakingChanges: true }));

const Banana = () => (
<View>
<TextInput
testID="bananaCustomFreshness"
placeholder={PLACEHOLDER_FRESHNESS}
value={INPUT_FRESHNESS}
/>
<TextInput
testID="bananaChef"
placeholder={PLACEHOLDER_CHEF}
value={INPUT_CHEF}
defaultValue={DEFAULT_INPUT_CHEF}
/>
<TextInput defaultValue={DEFAULT_INPUT_CUSTOMER} />
<TextInput defaultValue={'hello'} value="" />
</View>
);

test('getByDisplayValue, queryByDisplayValue', () => {
const { getByDisplayValue, queryByDisplayValue } = render(<Banana />);
const input = getByDisplayValue(/custom/i);

expect(input.props.value).toBe(INPUT_FRESHNESS);

const sameInput = getByDisplayValue(INPUT_FRESHNESS);

expect(sameInput.props.value).toBe(INPUT_FRESHNESS);
expect(() => getByDisplayValue('no value')).toThrow(
'Unable to find an element with displayValue: no value'
);

expect(queryByDisplayValue(/custom/i)).toBe(input);
expect(queryByDisplayValue('no value')).toBeNull();
expect(() => queryByDisplayValue(/fresh/i)).toThrow(
'Found multiple elements with display value: /fresh/i'
);
});

test('getByDisplayValue, queryByDisplayValue get element by default value only when value is undefined', () => {
const { getByDisplayValue, queryByDisplayValue } = render(<Banana />);
expect(() =>
getByDisplayValue(DEFAULT_INPUT_CHEF)
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with displayValue: What did you inspect?"`
);
expect(queryByDisplayValue(DEFAULT_INPUT_CHEF)).toBeNull();

expect(() => getByDisplayValue('hello')).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with displayValue: hello"`
);
expect(queryByDisplayValue('hello')).toBeNull();

expect(getByDisplayValue(DEFAULT_INPUT_CUSTOMER)).toBeTruthy();
expect(queryByDisplayValue(DEFAULT_INPUT_CUSTOMER)).toBeTruthy();
});

test('getAllByDisplayValue, queryAllByDisplayValue', () => {
const { getAllByDisplayValue, queryAllByDisplayValue } = render(<Banana />);
const inputs = getAllByDisplayValue(/fresh/i);

expect(inputs).toHaveLength(2);
expect(() => getAllByDisplayValue('no value')).toThrow(
'Unable to find an element with displayValue: no value'
);

expect(queryAllByDisplayValue(/fresh/i)).toEqual(inputs);
expect(queryAllByDisplayValue('no value')).toHaveLength(0);
});

test('findBy queries work asynchronously', async () => {
const options = { timeout: 10 }; // Short timeout so that this test runs quickly
const { rerender, findByDisplayValue, findAllByDisplayValue } = render(
<View />
);

await expect(
findByDisplayValue('Display Value', {}, options)
).rejects.toBeTruthy();
await expect(
findAllByDisplayValue('Display Value', {}, options)
).rejects.toBeTruthy();

setTimeout(
() =>
rerender(
<View>
<TextInput value="Display Value" />
</View>
),
20
);

await expect(findByDisplayValue('Display Value')).resolves.toBeTruthy();
await expect(findAllByDisplayValue('Display Value')).resolves.toHaveLength(1);
}, 20000);

test('byDisplayValue queries support hidden option', () => {
const { getByDisplayValue, queryByDisplayValue } = render(
<TextInput value="hidden" style={{ display: 'none' }} />
);

expect(getByDisplayValue('hidden')).toBeTruthy();
expect(
getByDisplayValue('hidden', { includeHiddenElements: true })
).toBeTruthy();

expect(
queryByDisplayValue('hidden', { includeHiddenElements: false })
).toBeFalsy();
expect(() =>
getByDisplayValue('hidden', { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with displayValue: hidden"`
);
});

test('byDisplayValue should return host component', () => {
const { getByDisplayValue } = render(<TextInput value="value" />);

expect(getByDisplayValue('value').type).toBe('TextInput');
});
Loading