Skip to content

Commit

Permalink
refactor: type all queries to return host components (#1426)
Browse files Browse the repository at this point in the history
* refactor: type all queries to return host components

* refactor: use brackets syntax for array types over Array

* refactor: change order of checks in findAll for perf

* refactor: hide host instance from public API

* refactor: make sure all queries return host elements

* Revert "refactor: make sure all queries return host elements"

This reverts commit 979f92a83e43b04a30c979445633896ec3496769.

* chore: rename to HostTestInstance

* refactor: remove flierNodeByType

* chore: fix codecov

---------

Co-authored-by: pierrezimmermann <[email protected]>
Co-authored-by: Maciej Jastrzębski <[email protected]>
  • Loading branch information
3 people authored Aug 11, 2023
1 parent 62a9b57 commit 2a0477e
Show file tree
Hide file tree
Showing 16 changed files with 66 additions and 92 deletions.
2 changes: 1 addition & 1 deletion src/fireEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
ScrollViewProps,
} from 'react-native';
import act from './act';
import { isPointerEventEnabled } from './helpers/pointer-events';
import { isHostElement } from './helpers/component-tree';
import { isHostTextInput } from './helpers/host-component-names';
import { isPointerEventEnabled } from './helpers/pointer-events';

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

Expand Down
23 changes: 14 additions & 9 deletions src/helpers/component-tree.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { ReactTestInstance } from 'react-test-renderer';

/**
* ReactTestInstance referring to host element.
*/
export type HostTestInstance = ReactTestInstance & { type: string };

/**
* Checks if the given element is a host element.
* @param element The element to check.
*/
export function isHostElement(element?: ReactTestInstance | null) {
export function isHostElement(
element?: ReactTestInstance | null
): element is HostTestInstance {
return typeof element?.type === 'string';
}

Expand All @@ -14,7 +21,7 @@ export function isHostElement(element?: ReactTestInstance | null) {
*/
export function getHostParent(
element: ReactTestInstance | null
): ReactTestInstance | null {
): HostTestInstance | null {
if (element == null) {
return null;
}
Expand All @@ -37,12 +44,12 @@ export function getHostParent(
*/
export function getHostChildren(
element: ReactTestInstance | null
): ReactTestInstance[] {
): HostTestInstance[] {
if (element == null) {
return [];
}

const hostChildren: ReactTestInstance[] = [];
const hostChildren: HostTestInstance[] = [];

element.children.forEach((child) => {
if (typeof child !== 'object') {
Expand All @@ -68,10 +75,8 @@ export function getHostChildren(
*/
export function getHostSelves(
element: ReactTestInstance | null
): ReactTestInstance[] {
return typeof element?.type === 'string'
? [element]
: getHostChildren(element);
): HostTestInstance[] {
return isHostElement(element) ? [element] : getHostChildren(element);
}

/**
Expand All @@ -80,7 +85,7 @@ export function getHostSelves(
*/
export function getHostSiblings(
element: ReactTestInstance | null
): ReactTestInstance[] {
): HostTestInstance[] {
const hostParent = getHostParent(element);
const hostSelves = getHostSelves(element);
return getHostChildren(hostParent).filter(
Expand Down
7 changes: 0 additions & 7 deletions src/helpers/filterNodeByType.ts

This file was deleted.

10 changes: 6 additions & 4 deletions src/helpers/findAll.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ReactTestInstance } from 'react-test-renderer';
import { getConfig } from '../config';
import { isHiddenFromAccessibility } from './accessiblity';
import { HostTestInstance, isHostElement } from './component-tree';

interface FindAllOptions {
/** Match elements hidden from accessibility */
Expand All @@ -17,7 +18,7 @@ export function findAll(
root: ReactTestInstance,
predicate: (element: ReactTestInstance) => boolean,
options?: FindAllOptions
) {
): HostTestInstance[] {
const results = findAllInternal(root, predicate, options);

const includeHiddenElements =
Expand All @@ -41,11 +42,11 @@ function findAllInternal(
root: ReactTestInstance,
predicate: (element: ReactTestInstance) => boolean,
options?: FindAllOptions
): Array<ReactTestInstance> {
const results: ReactTestInstance[] = [];
): HostTestInstance[] {
const results: HostTestInstance[] = [];

// Match descendants first but do not add them to results yet.
const matchingDescendants: ReactTestInstance[] = [];
const matchingDescendants: HostTestInstance[] = [];
root.children.forEach((child) => {
if (typeof child === 'string') {
return;
Expand All @@ -56,6 +57,7 @@ function findAllInternal(
if (
// When matchDeepestOnly = true: add current element only if no descendants match
(!options?.matchDeepestOnly || matchingDescendants.length === 0) &&
isHostElement(root) &&
predicate(root)
) {
results.push(root);
Expand Down
17 changes: 15 additions & 2 deletions src/helpers/host-component-names.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ReactTestInstance } from 'react-test-renderer';
import { Switch, Text, TextInput, View } from 'react-native';
import { configureInternal, getConfig, HostComponentNames } from '../config';
import { renderWithAct } from '../render-act';
import { HostTestInstance } from './component-tree';

const userConfigErrorMessage = `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.`;
Expand Down Expand Up @@ -66,10 +67,22 @@ function getByTestId(instance: ReactTestInstance, testID: string) {
return nodes[0];
}

export function isHostText(element?: ReactTestInstance) {
/**
* Checks if the given element is a host Text.
* @param element The element to check.
*/
export function isHostText(
element?: ReactTestInstance | null
): element is HostTestInstance {
return element?.type === getHostComponentNames().text;
}

export function isHostTextInput(element?: ReactTestInstance) {
/**
* Checks if the given element is a host TextInput.
* @param element The element to check.
*/
export function isHostTextInput(
element?: ReactTestInstance | null
): element is HostTestInstance {
return element?.type === getHostComponentNames().textInput;
}
1 change: 0 additions & 1 deletion src/helpers/matchers/matchLabelText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ function matchAccessibilityLabelledBy(
findAll(
root,
(node) =>
typeof node.type === 'string' &&
node.props.nativeID === nativeId &&
matchTextContent(node, text, options)
).length > 0
Expand Down
8 changes: 2 additions & 6 deletions src/queries/a11yState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,11 @@ import { CommonQueryOptions } from './options';

const queryAllByA11yState = (
instance: ReactTestInstance
): ((
matcher: AccessibilityStateMatcher,
queryOptions?: CommonQueryOptions
) => Array<ReactTestInstance>) =>
): QueryAllByQuery<AccessibilityStateMatcher, CommonQueryOptions> =>
function queryAllByA11yStateFn(matcher, queryOptions) {
return findAll(
instance,
(node) =>
typeof node.type === 'string' && matchAccessibilityState(node, matcher),
(node) => matchAccessibilityState(node, matcher),
queryOptions
);
};
Expand Down
8 changes: 2 additions & 6 deletions src/queries/a11yValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,11 @@ import { CommonQueryOptions } from './options';

const queryAllByA11yValue = (
instance: ReactTestInstance
): ((
value: AccessibilityValueMatcher,
queryOptions?: CommonQueryOptions
) => Array<ReactTestInstance>) =>
): QueryAllByQuery<AccessibilityValueMatcher, CommonQueryOptions> =>
function queryAllByA11yValueFn(value, queryOptions) {
return findAll(
instance,
(node) =>
typeof node.type === 'string' && matchAccessibilityValue(node, value),
(node) => matchAccessibilityValue(node, value),
queryOptions
);
};
Expand Down
21 changes: 7 additions & 14 deletions src/queries/displayValue.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { filterNodeByType } from '../helpers/filterNodeByType';
import { findAll } from '../helpers/findAll';
import { isHostTextInput } from '../helpers/host-component-names';
import { matches, TextMatch, TextMatchOptions } from '../matches';
import { getHostComponentNames } from '../helpers/host-component-names';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
Expand All @@ -16,32 +15,26 @@ import type { CommonQueryOptions } from './options';

type ByDisplayValueOptions = CommonQueryOptions & TextMatchOptions;

const getTextInputNodeByDisplayValue = (
const matchDisplayValue = (
node: ReactTestInstance,
value: TextMatch,
options: TextMatchOptions = {}
) => {
const { exact, normalizer } = options;
const nodeValue =
node.props.value !== undefined ? node.props.value : node.props.defaultValue;
const nodeValue = node.props.value ?? node.props.defaultValue;

return (
filterNodeByType(node, getHostComponentNames().textInput) &&
matches(value, nodeValue, normalizer, exact)
);
return matches(value, nodeValue, normalizer, exact);
};

const queryAllByDisplayValue = (
instance: ReactTestInstance
): ((
displayValue: TextMatch,
queryOptions?: ByDisplayValueOptions
) => Array<ReactTestInstance>) =>
): QueryAllByQuery<TextMatch, ByDisplayValueOptions> =>
function queryAllByDisplayValueFn(displayValue, queryOptions) {
return findAll(
instance,
(node) =>
getTextInputNodeByDisplayValue(node, displayValue, queryOptions),
isHostTextInput(node) &&
matchDisplayValue(node, displayValue, queryOptions),
queryOptions
);
};
Expand Down
9 changes: 2 additions & 7 deletions src/queries/hintText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,11 @@ const getNodeByHintText = (

const queryAllByHintText = (
instance: ReactTestInstance
): ((
hint: TextMatch,
queryOptions?: ByHintTextOptions
) => Array<ReactTestInstance>) =>
): QueryAllByQuery<TextMatch, ByHintTextOptions> =>
function queryAllByA11yHintFn(hint, queryOptions) {
return findAll(
instance,
(node) =>
typeof node.type === 'string' &&
getNodeByHintText(node, hint, queryOptions),
(node) => getNodeByHintText(node, hint, queryOptions),
queryOptions
);
};
Expand Down
4 changes: 1 addition & 3 deletions src/queries/labelText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ function queryAllByLabelText(instance: ReactTestInstance) {
return (text: TextMatch, queryOptions?: ByLabelTextOptions) => {
return findAll(
instance,
(node) =>
typeof node.type === 'string' &&
matchLabelText(instance, node, text, queryOptions),
(node) => matchLabelText(instance, node, text, queryOptions),
queryOptions
);
};
Expand Down
19 changes: 6 additions & 13 deletions src/queries/placeholderText.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { findAll } from '../helpers/findAll';
import { matches, TextMatch, TextMatchOptions } from '../matches';
import { filterNodeByType } from '../helpers/filterNodeByType';
import { getHostComponentNames } from '../helpers/host-component-names';
import { isHostTextInput } from '../helpers/host-component-names';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
Expand All @@ -16,30 +15,24 @@ import type { CommonQueryOptions } from './options';

type ByPlaceholderTextOptions = CommonQueryOptions & TextMatchOptions;

const getTextInputNodeByPlaceholderText = (
const matchPlaceholderText = (
node: ReactTestInstance,
placeholder: TextMatch,
options: TextMatchOptions = {}
) => {
const { exact, normalizer } = options;

return (
filterNodeByType(node, getHostComponentNames().textInput) &&
matches(placeholder, node.props.placeholder, normalizer, exact)
);
return matches(placeholder, node.props.placeholder, normalizer, exact);
};

const queryAllByPlaceholderText = (
instance: ReactTestInstance
): ((
placeholder: TextMatch,
queryOptions?: ByPlaceholderTextOptions
) => Array<ReactTestInstance>) =>
): QueryAllByQuery<TextMatch, ByPlaceholderTextOptions> =>
function queryAllByPlaceholderFn(placeholder, queryOptions) {
return findAll(
instance,
(node) =>
getTextInputNodeByPlaceholderText(node, placeholder, queryOptions),
isHostTextInput(node) &&
matchPlaceholderText(node, placeholder, queryOptions),
queryOptions
);
};
Expand Down
3 changes: 1 addition & 2 deletions src/queries/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,12 @@ const matchAccessibilityValueIfNeeded = (

const queryAllByRole = (
instance: ReactTestInstance
): ((role: TextMatch, options?: ByRoleOptions) => Array<ReactTestInstance>) =>
): QueryAllByQuery<TextMatch, ByRoleOptions> =>
function queryAllByRoleFn(role, options) {
return findAll(
instance,
(node) =>
// run the cheapest checks first, and early exit to avoid unneeded computations
typeof node.type === 'string' &&
isAccessibilityElement(node) &&
matchStringProp(node.props.accessibilityRole, role) &&
matchAccessibleStateIfNeeded(node, options) &&
Expand Down
15 changes: 5 additions & 10 deletions src/queries/testId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,22 @@ import type { CommonQueryOptions } from './options';

type ByTestIdOptions = CommonQueryOptions & TextMatchOptions;

const getNodeByTestId = (
const matchTestId = (
node: ReactTestInstance,
testID: TextMatch,
testId: TextMatch,
options: TextMatchOptions = {}
) => {
const { exact, normalizer } = options;
return matches(testID, node.props.testID, normalizer, exact);
return matches(testId, node.props.testID, normalizer, exact);
};

const queryAllByTestId = (
instance: ReactTestInstance
): ((
testId: TextMatch,
queryOptions?: ByTestIdOptions
) => Array<ReactTestInstance>) =>
): QueryAllByQuery<TextMatch, ByTestIdOptions> =>
function queryAllByTestIdFn(testId, queryOptions) {
return findAll(
instance,
(node) =>
typeof node.type === 'string' &&
getNodeByTestId(node, testId, queryOptions),
(node) => matchTestId(node, testId, queryOptions),
queryOptions
);
};
Expand Down
9 changes: 3 additions & 6 deletions src/queries/text.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { filterNodeByType } from '../helpers/filterNodeByType';
import { findAll } from '../helpers/findAll';
import { getHostComponentNames } from '../helpers/host-component-names';
import { isHostText } from '../helpers/host-component-names';
import { matchTextContent } from '../helpers/matchers/matchTextContent';
import { TextMatch, TextMatchOptions } from '../matches';
import { makeQueries } from './makeQueries';
Expand All @@ -19,13 +18,11 @@ type ByTextOptions = CommonQueryOptions & TextMatchOptions;

const queryAllByText = (
instance: ReactTestInstance
): ((text: TextMatch, options?: ByTextOptions) => Array<ReactTestInstance>) =>
): QueryAllByQuery<TextMatch, ByTextOptions> =>
function queryAllByTextFn(text, options = {}) {
return findAll(
instance,
(node) =>
filterNodeByType(node, getHostComponentNames().text) &&
matchTextContent(node, text, options),
(node) => isHostText(node) && matchTextContent(node, text, options),
{
...options,
matchDeepestOnly: true,
Expand Down
Loading

0 comments on commit 2a0477e

Please sign in to comment.