From 4e06bea68cd116b352f5818c991d09a77e04d8c9 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 1 May 2022 16:26:43 +0200 Subject: [PATCH] chore: make queries by text based on the tree --- src/__tests__/byText.test.tsx | 18 +++++ src/helpers/byDisplayValue.ts | 6 +- src/helpers/byPlaceholderText.ts | 6 +- src/helpers/byTestId.ts | 6 +- src/helpers/byText.ts | 133 +++++++++++++++++++++---------- src/helpers/findByAPI.ts | 20 ++--- src/helpers/getByAPI.ts | 28 +++---- src/helpers/makeQueries.ts | 26 +++--- src/helpers/queryByAPI.ts | 28 +++---- src/render.tsx | 6 +- 10 files changed, 170 insertions(+), 107 deletions(-) diff --git a/src/__tests__/byText.test.tsx b/src/__tests__/byText.test.tsx index 22edd1f7e..0602f6ab8 100644 --- a/src/__tests__/byText.test.tsx +++ b/src/__tests__/byText.test.tsx @@ -473,3 +473,21 @@ test('getByText and queryByText work properly with multiple nested fragments', ( expect(getByText('Hello')).toBeTruthy(); expect(queryByText('Hello')).not.toBeNull(); }); + +const TranslationText = ({ + translationKey, +}: { + translationKey: string; +}): React.ReactElement => { + return <>{translationKey}; +}; + +test('it should be abdle to find text nested in components that render as string', () => { + const { getByText } = render( + + + + ); + + expect(getByText('hello')).toBeTruthy(); +}); diff --git a/src/helpers/byDisplayValue.ts b/src/helpers/byDisplayValue.ts index da89a6390..4aa6a4e0d 100644 --- a/src/helpers/byDisplayValue.ts +++ b/src/helpers/byDisplayValue.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer'; import { matches, TextMatch } from '../matches'; import { makeQueries } from './makeQueries'; import type { Queries } from './makeQueries'; @@ -28,13 +28,13 @@ const getTextInputNodeByDisplayValue = ( }; const queryAllByDisplayValue = ( - instance: ReactTestInstance + renderer: ReactTestRenderer ): (( displayValue: TextMatch, queryOptions?: TextMatchOptions ) => Array) => function queryAllByDisplayValueFn(displayValue, queryOptions) { - return instance.findAll((node) => + return renderer.root.findAll((node) => getTextInputNodeByDisplayValue(node, displayValue, queryOptions) ); }; diff --git a/src/helpers/byPlaceholderText.ts b/src/helpers/byPlaceholderText.ts index 2145ef176..7ba24ea9e 100644 --- a/src/helpers/byPlaceholderText.ts +++ b/src/helpers/byPlaceholderText.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer'; import { matches, TextMatch } from '../matches'; import { makeQueries } from './makeQueries'; import type { Queries } from './makeQueries'; @@ -24,13 +24,13 @@ const getTextInputNodeByPlaceholderText = ( }; const queryAllByPlaceholderText = ( - instance: ReactTestInstance + renderer: ReactTestRenderer ): (( placeholder: TextMatch, queryOptions?: TextMatchOptions ) => Array) => function queryAllByPlaceholderFn(placeholder, queryOptions) { - return instance.findAll((node) => + return renderer.root.findAll((node) => getTextInputNodeByPlaceholderText(node, placeholder, queryOptions) ); }; diff --git a/src/helpers/byTestId.ts b/src/helpers/byTestId.ts index e14e833e2..0d078c75e 100644 --- a/src/helpers/byTestId.ts +++ b/src/helpers/byTestId.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer'; import { matches, TextMatch } from '../matches'; import { makeQueries } from './makeQueries'; import type { Queries } from './makeQueries'; @@ -14,13 +14,13 @@ const getNodeByTestId = ( }; const queryAllByTestId = ( - instance: ReactTestInstance + renderer: ReactTestRenderer ): (( testId: TextMatch, queryOptions?: TextMatchOptions ) => Array) => function queryAllByTestIdFn(testId, queryOptions) { - const results = instance + const results = renderer.root .findAll((node) => getNodeByTestId(node, testId, queryOptions)) .filter((element) => typeof element.type === 'string'); diff --git a/src/helpers/byText.ts b/src/helpers/byText.ts index 1d9249798..d000bd2c8 100644 --- a/src/helpers/byText.ts +++ b/src/helpers/byText.ts @@ -1,10 +1,13 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { + ReactTestInstance, + ReactTestRenderer, + ReactTestRendererTree, +} from 'react-test-renderer'; import * as React from 'react'; import { matches, TextMatch } from '../matches'; import type { NormalizerFn } from '../matches'; import { makeQueries } from './makeQueries'; import type { Queries } from './makeQueries'; -import { filterNodeByType } from './filterNodeByType'; import { createLibraryNotSupportedError } from './errors'; export type TextMatchOptions = { @@ -13,75 +16,117 @@ export type TextMatchOptions = { }; const getChildrenAsText = ( - children: React.ReactChild[], + children: ReactTestRendererTree | ReactTestRendererTree[] | null, TextComponent: React.ComponentType -) => { - const textContent: string[] = []; - React.Children.forEach(children, (child) => { - if (typeof child === 'string') { - textContent.push(child); - return; - } +): string[] => { + if (!children) { + return []; + } - if (typeof child === 'number') { - textContent.push(child.toString()); - return; + if (typeof children === 'string') { + return [children]; + } + + const textContent: string[] = []; + if (!Array.isArray(children)) { + // Bail on traversing text children down the tree if current node (child) + // has no text. In such situations, react-test-renderer will traverse down + // this tree in a separate call and run this query again. As a result, the + // query will match the deepest text node that matches requested text. + if (children.type === 'Text') { + return []; } - if (child?.props?.children) { - // Bail on traversing text children down the tree if current node (child) - // has no text. In such situations, react-test-renderer will traverse down - // this tree in a separate call and run this query again. As a result, the - // query will match the deepest text node that matches requested text. - if (filterNodeByType(child, TextComponent)) { - return; - } + return getChildrenAsText(children.rendered, TextComponent); + } - if (filterNodeByType(child, React.Fragment)) { - textContent.push( - ...getChildrenAsText(child.props.children, TextComponent) - ); - } - } - }); + children.forEach((child) => + textContent.push(...getChildrenAsText(child, TextComponent)) + ); return textContent; }; -const getNodeByText = ( - node: ReactTestInstance, +const getInstancesByText = ( + tree: ReactTestRendererTree, text: TextMatch, options: TextMatchOptions = {} -) => { +): Omit[] => { + const instances: Omit[] = []; + try { + if (!tree.rendered) { + return []; + } + + if (!tree.instance) { + if (!Array.isArray(tree.rendered)) { + return [...getInstancesByText(tree.rendered, text, options)]; + } + + tree.rendered.forEach((rendered) => { + instances.push(...getInstancesByText(rendered, text, options)); + }); + return instances; + } + + const textChildren: string[] = []; const { Text } = require('react-native'); - const isTextComponent = filterNodeByType(node, Text); - if (isTextComponent) { - const textChildren = getChildrenAsText(node.props.children, Text); - if (textChildren) { - const textToTest = textChildren.join(''); - const { exact, normalizer } = options; - return matches(text, textToTest, normalizer, exact); + if (!Array.isArray(tree.rendered)) { + if (tree.rendered.type === 'Text') { + textChildren.push(...getChildrenAsText(tree.rendered.rendered, Text)); } + instances.push(...getInstancesByText(tree.rendered, text, options)); + } else { + tree.rendered.forEach((child) => { + if (child.type === 'Text') { + textChildren.push(...getChildrenAsText(child, Text)); + } else { + instances.push(...getInstancesByText(child, text, options)); + } + }); } - return false; + + if (textChildren.length) { + const textToTest = textChildren.join(''); + const { exact, normalizer } = options; + if (matches(text, textToTest, normalizer, exact)) { + instances.push(tree.instance); + } + } + + return instances; } catch (error) { throw createLibraryNotSupportedError(error); } }; const queryAllByText = ( - instance: ReactTestInstance + renderer: ReactTestRenderer ): (( text: TextMatch, queryOptions?: TextMatchOptions ) => Array) => function queryAllByTextFn(text, queryOptions) { - const results = instance.findAll((node) => - getNodeByText(node, text, queryOptions) - ); + const tree = renderer.toTree(); + const treeInstance = renderer.root; + + if (!tree) { + return []; + } + + const instancesFound = getInstancesByText(tree, text, queryOptions); - return results; + // Instances in the tree are not of type ReactTestInstance because they do not have parent + // This is problematic when firing events so we find in the root nodes the one that matches + return instancesFound.map((instanceFound) => { + return treeInstance.find((treeInstance) => { + return ( + treeInstance.instance && + treeInstance.instance._reactInternals.stateNode === instanceFound + ); + }); + }); }; const getMultipleError = (text: TextMatch) => diff --git a/src/helpers/findByAPI.ts b/src/helpers/findByAPI.ts index 551008137..47f87d949 100644 --- a/src/helpers/findByAPI.ts +++ b/src/helpers/findByAPI.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer'; import type { WaitForOptions } from '../waitFor'; import type { TextMatch } from '../matches'; import type { TextMatchOptions } from './byText'; @@ -56,15 +56,15 @@ export type FindByAPI = { ) => Promise; }; -export const findByAPI = (instance: ReactTestInstance): FindByAPI => ({ - findByTestId: findByTestId(instance), - findByText: findByText(instance), - findByPlaceholderText: findByPlaceholderText(instance), - findByDisplayValue: findByDisplayValue(instance), - findAllByTestId: findAllByTestId(instance), - findAllByText: findAllByText(instance), - findAllByPlaceholderText: findAllByPlaceholderText(instance), - findAllByDisplayValue: findAllByDisplayValue(instance), +export const findByAPI = (renderer: ReactTestRenderer): FindByAPI => ({ + findByTestId: findByTestId(renderer), + findByText: findByText(renderer), + findByPlaceholderText: findByPlaceholderText(renderer), + findByDisplayValue: findByDisplayValue(renderer), + findAllByTestId: findAllByTestId(renderer), + findAllByText: findAllByText(renderer), + findAllByPlaceholderText: findAllByPlaceholderText(renderer), + findAllByDisplayValue: findAllByDisplayValue(renderer), // Renamed findByPlaceholder: () => diff --git a/src/helpers/getByAPI.ts b/src/helpers/getByAPI.ts index 0c816e9df..294be497c 100644 --- a/src/helpers/getByAPI.ts +++ b/src/helpers/getByAPI.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer'; import * as React from 'react'; import prettyFormat from 'pretty-format'; import type { TextMatch } from '../matches'; @@ -104,19 +104,19 @@ export const UNSAFE_getAllByProps = ( return results; }; -export const getByAPI = (instance: ReactTestInstance): GetByAPI => ({ - getByText: getByText(instance), - getByPlaceholderText: getByPlaceholderText(instance), - getByDisplayValue: getByDisplayValue(instance), - getByTestId: getByTestId(instance), - getAllByText: getAllByText(instance), - getAllByPlaceholderText: getAllByPlaceholderText(instance), - getAllByDisplayValue: getAllByDisplayValue(instance), - getAllByTestId: getAllByTestId(instance), +export const getByAPI = (renderer: ReactTestRenderer): GetByAPI => ({ + getByText: getByText(renderer), + getByPlaceholderText: getByPlaceholderText(renderer), + getByDisplayValue: getByDisplayValue(renderer), + getByTestId: getByTestId(renderer), + getAllByText: getAllByText(renderer), + getAllByPlaceholderText: getAllByPlaceholderText(renderer), + getAllByDisplayValue: getAllByDisplayValue(renderer), + getAllByTestId: getAllByTestId(renderer), // Unsafe - UNSAFE_getByType: UNSAFE_getByType(instance), - UNSAFE_getAllByType: UNSAFE_getAllByType(instance), - UNSAFE_getByProps: UNSAFE_getByProps(instance), - UNSAFE_getAllByProps: UNSAFE_getAllByProps(instance), + UNSAFE_getByType: UNSAFE_getByType(renderer.root), + UNSAFE_getAllByType: UNSAFE_getAllByType(renderer.root), + UNSAFE_getByProps: UNSAFE_getByProps(renderer.root), + UNSAFE_getAllByProps: UNSAFE_getAllByProps(renderer.root), }); diff --git a/src/helpers/makeQueries.ts b/src/helpers/makeQueries.ts index 78dd61661..e117b849f 100644 --- a/src/helpers/makeQueries.ts +++ b/src/helpers/makeQueries.ts @@ -1,15 +1,15 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer'; import waitFor from '../waitFor'; import type { WaitForOptions } from '../waitFor'; import { ErrorWithStack } from './errors'; import type { TextMatchOptions } from './byText'; type QueryFunction = ( - instance: ReactTestInstance + tree: ReactTestRenderer ) => (args: ArgType, queryOptions?: TextMatchOptions) => ReturnType; type FindQueryFunction = ( - instance: ReactTestInstance + tree: ReactTestRenderer ) => ( args: ArgType, queryOptions?: TextMatchOptions & WaitForOptions, @@ -77,9 +77,9 @@ export function makeQueries( getMissingError: (args: QueryArg) => string, getMultipleError: (args: QueryArg) => string ): Queries { - function getAllByQuery(instance: ReactTestInstance) { + function getAllByQuery(tree: ReactTestRenderer) { return function getAllFn(args: QueryArg, queryOptions?: TextMatchOptions) { - const results = queryAllByQuery(instance)(args, queryOptions); + const results = queryAllByQuery(tree)(args, queryOptions); if (results.length === 0) { throw new ErrorWithStack(getMissingError(args), getAllFn); @@ -89,12 +89,12 @@ export function makeQueries( }; } - function queryByQuery(instance: ReactTestInstance) { + function queryByQuery(tree: ReactTestRenderer) { return function singleQueryFn( args: QueryArg, queryOptions?: TextMatchOptions ) { - const results = queryAllByQuery(instance)(args, queryOptions); + const results = queryAllByQuery(tree)(args, queryOptions); if (results.length > 1) { throw new ErrorWithStack(getMultipleError(args), singleQueryFn); @@ -108,9 +108,9 @@ export function makeQueries( }; } - function getByQuery(instance: ReactTestInstance) { + function getByQuery(tree: ReactTestRenderer) { return function getFn(args: QueryArg, queryOptions?: TextMatchOptions) { - const results = queryAllByQuery(instance)(args, queryOptions); + const results = queryAllByQuery(tree)(args, queryOptions); if (results.length > 1) { throw new ErrorWithStack(getMultipleError(args), getFn); @@ -124,7 +124,7 @@ export function makeQueries( }; } - function findAllByQuery(instance: ReactTestInstance) { + function findAllByQuery(tree: ReactTestRenderer) { return function findAllFn( args: QueryArg, queryOptions?: TextMatchOptions & WaitForOptions, @@ -133,14 +133,14 @@ export function makeQueries( const deprecatedWaitForOptions = extractDeprecatedWaitForOptionUsage( queryOptions ); - return waitFor(() => getAllByQuery(instance)(args, queryOptions), { + return waitFor(() => getAllByQuery(tree)(args, queryOptions), { ...deprecatedWaitForOptions, ...waitForOptions, }); }; } - function findByQuery(instance: ReactTestInstance) { + function findByQuery(tree: ReactTestRenderer) { return function findFn( args: QueryArg, queryOptions?: TextMatchOptions & WaitForOptions, @@ -149,7 +149,7 @@ export function makeQueries( const deprecatedWaitForOptions = extractDeprecatedWaitForOptionUsage( queryOptions ); - return waitFor(() => getByQuery(instance)(args, queryOptions), { + return waitFor(() => getByQuery(tree)(args, queryOptions), { ...deprecatedWaitForOptions, ...waitForOptions, }); diff --git a/src/helpers/queryByAPI.ts b/src/helpers/queryByAPI.ts index 12f73ecc5..c28b73931 100644 --- a/src/helpers/queryByAPI.ts +++ b/src/helpers/queryByAPI.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer'; import * as React from 'react'; import type { TextMatch } from '../matches'; import type { TextMatchOptions } from './byText'; @@ -119,21 +119,21 @@ export const UNSAFE_queryAllByProps = ( } }; -export const queryByAPI = (instance: ReactTestInstance): QueryByAPI => ({ - queryByTestId: queryByTestId(instance), - queryByText: queryByText(instance), - queryByPlaceholderText: queryByPlaceholderText(instance), - queryByDisplayValue: queryByDisplayValue(instance), - queryAllByTestId: queryAllByTestId(instance), - queryAllByText: queryAllByText(instance), - queryAllByPlaceholderText: queryAllByPlaceholderText(instance), - queryAllByDisplayValue: queryAllByDisplayValue(instance), +export const queryByAPI = (renderer: ReactTestRenderer): QueryByAPI => ({ + queryByTestId: queryByTestId(renderer), + queryByText: queryByText(renderer), + queryByPlaceholderText: queryByPlaceholderText(renderer), + queryByDisplayValue: queryByDisplayValue(renderer), + queryAllByTestId: queryAllByTestId(renderer), + queryAllByText: queryAllByText(renderer), + queryAllByPlaceholderText: queryAllByPlaceholderText(renderer), + queryAllByDisplayValue: queryAllByDisplayValue(renderer), // Unsafe - UNSAFE_queryByType: UNSAFE_queryByType(instance), - UNSAFE_queryAllByType: UNSAFE_queryAllByType(instance), - UNSAFE_queryByProps: UNSAFE_queryByProps(instance), - UNSAFE_queryAllByProps: UNSAFE_queryAllByProps(instance), + UNSAFE_queryByType: UNSAFE_queryByType(renderer.root), + UNSAFE_queryAllByType: UNSAFE_queryAllByType(renderer.root), + UNSAFE_queryByProps: UNSAFE_queryByProps(renderer.root), + UNSAFE_queryAllByProps: UNSAFE_queryAllByProps(renderer.root), // Removed queryByName: () => diff --git a/src/render.tsx b/src/render.tsx index 1f446900a..8491811e2 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -44,9 +44,9 @@ export default function render( addToCleanupQueue(unmount); return { - ...getByAPI(instance), - ...queryByAPI(instance), - ...findByAPI(instance), + ...getByAPI(renderer), + ...queryByAPI(renderer), + ...findByAPI(renderer), ...a11yAPI(instance), update, unmount,