From 0bd9947f1bd2d57e9d2a729678bf49ed5f2822f3 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 31 Oct 2024 10:06:05 +0100 Subject: [PATCH 1/2] refactor(v13): remove host component name detection --- src/__tests__/config.test.ts | 22 +-- src/__tests__/host-component-names.test.tsx | 147 +++++--------------- src/__tests__/render.test.tsx | 11 +- src/config.ts | 16 +-- src/fire-event.ts | 2 +- src/helpers/accessibility.ts | 15 +- src/helpers/host-component-names.ts | 57 ++++++++ src/helpers/host-component-names.tsx | 120 ---------------- src/render-hook.tsx | 1 - src/render.tsx | 19 +-- 10 files changed, 104 insertions(+), 306 deletions(-) create mode 100644 src/helpers/host-component-names.ts delete mode 100644 src/helpers/host-component-names.tsx diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index d20f91707..50b955675 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -34,27 +34,11 @@ test('resetToDefaults() resets config to defaults', () => { }); test('resetToDefaults() resets internal config to defaults', () => { - configureInternal({ - hostComponentNames: { - text: 'A', - textInput: 'A', - image: 'A', - switch: 'A', - scrollView: 'A', - modal: 'A', - }, - }); - expect(getConfig().hostComponentNames).toEqual({ - text: 'A', - textInput: 'A', - image: 'A', - switch: 'A', - scrollView: 'A', - modal: 'A', - }); + configureInternal({ asyncUtilTimeout: 2000 }); + expect(getConfig().asyncUtilTimeout).toBe(2000); resetToDefaults(); - expect(getConfig().hostComponentNames).toBe(undefined); + expect(getConfig().asyncUtilTimeout).toBe(1000); }); test('configure handles alias option defaultHidden', () => { diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 0e55f1a82..d3050c8ec 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -1,123 +1,48 @@ import * as React from 'react'; -import { View } from 'react-native'; -import TestRenderer from 'react-test-renderer'; -import { configureInternal, getConfig } from '../config'; +import { Image, Modal, ScrollView, Switch, Text, TextInput } from 'react-native'; import { - getHostComponentNames, - configureHostComponentNamesIfNeeded, + isHostImage, + isHostModal, + isHostScrollView, + isHostSwitch, + isHostText, + isHostTextInput, } from '../helpers/host-component-names'; -import { act, render } from '..'; +import { render, screen } from '..'; -describe('getHostComponentNames', () => { - test('returns host component names from internal config', () => { - configureInternal({ - hostComponentNames: { - text: 'banana', - textInput: 'banana', - image: 'banana', - switch: 'banana', - scrollView: 'banana', - modal: 'banana', - }, - }); - - expect(getHostComponentNames()).toEqual({ - text: 'banana', - textInput: 'banana', - image: 'banana', - switch: 'banana', - scrollView: 'banana', - modal: 'banana', - }); - }); - - test('detects host component names if not present in internal config', () => { - expect(getConfig().hostComponentNames).toBeUndefined(); - - const hostComponentNames = getHostComponentNames(); - - expect(hostComponentNames).toEqual({ - text: 'Text', - textInput: 'TextInput', - image: 'Image', - switch: 'RCTSwitch', - scrollView: 'RCTScrollView', - modal: 'Modal', - }); - expect(getConfig().hostComponentNames).toBe(hostComponentNames); - }); - - // Repro test for case when user indirectly triggers `getHostComponentNames` calls from - // explicit `act` wrapper. - // See: https://github.com/callstack/react-native-testing-library/issues/1302 - // and https://github.com/callstack/react-native-testing-library/issues/1305 - test('does not throw when wrapped in act after render has been called', () => { - render(); - expect(() => - act(() => { - getHostComponentNames(); - }), - ).not.toThrow(); - }); +test('detects host Text component', () => { + render(Hello); + expect(isHostText(screen.root)).toBe(true); }); -describe('configureHostComponentNamesIfNeeded', () => { - test('updates internal config with host component names when they are not defined', () => { - expect(getConfig().hostComponentNames).toBeUndefined(); - - configureHostComponentNamesIfNeeded(); - - expect(getConfig().hostComponentNames).toEqual({ - text: 'Text', - textInput: 'TextInput', - image: 'Image', - switch: 'RCTSwitch', - scrollView: 'RCTScrollView', - modal: 'Modal', - }); - }); - - test('does not update internal config when host component names are already configured', () => { - configureInternal({ - hostComponentNames: { - text: 'banana', - textInput: 'banana', - image: 'banana', - switch: 'banana', - scrollView: 'banana', - modal: 'banana', - }, - }); - - configureHostComponentNamesIfNeeded(); - - expect(getConfig().hostComponentNames).toEqual({ - text: 'banana', - textInput: 'banana', - image: 'banana', - switch: 'banana', - scrollView: 'banana', - modal: 'banana', - }); - }); - - test('throw an error when auto-detection fails', () => { - const mockCreate = jest.spyOn(TestRenderer, 'create') as jest.Mock; - const renderer = TestRenderer.create(); +// Some users might use the raw RCTText component directly for performance reasons. +// See: https://blog.theodo.com/2023/10/native-views-rn-performance/ +test('detects raw RCTText component', () => { + render(React.createElement('RCTText', { testID: 'text' }, 'Hello')); + expect(isHostText(screen.root)).toBe(true); +}); - mockCreate.mockReturnValue({ - root: renderer.root, - }); +test('detects host TextInput component', () => { + render(); + expect(isHostTextInput(screen.root)).toBe(true); +}); - expect(() => configureHostComponentNamesIfNeeded()).toThrowErrorMatchingInlineSnapshot(` - "Trying to detect host component names triggered the following error: +test('detects host Image component', () => { + render(); + expect(isHostImage(screen.root)).toBe(true); +}); - Unable to find an element with testID: text +test('detects host Switch component', () => { + render(); + expect(isHostSwitch(screen.root)).toBe(true); +}); - 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." - `); +test('detects host ScrollView component', () => { + render(); + expect(isHostScrollView(screen.root)).toBe(true); +}); - mockCreate.mockReset(); - }); +test('detects host Modal component', () => { + render(); + expect(isHostModal(screen.root)).toBe(true); }); diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index fae79012b..58acb4535 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -1,7 +1,6 @@ /* eslint-disable no-console */ import * as React from 'react'; import { Pressable, Text, TextInput, View } from 'react-native'; -import { getConfig, resetToDefaults } from '../config'; import { fireEvent, render, RenderAPI, screen } from '..'; const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; @@ -234,17 +233,9 @@ test('returned output can be spread using rest operator', () => { expect(rest).toBeTruthy(); }); -test('render calls detects host component names', () => { - resetToDefaults(); - expect(getConfig().hostComponentNames).toBeUndefined(); - - render(); - expect(getConfig().hostComponentNames).not.toBeUndefined(); -}); - test('supports legacy rendering', () => { render(, { concurrentRoot: false }); - expect(screen.root).toBeDefined(); + expect(screen.root).toBeOnTheScreen(); }); test('supports concurrent rendering', () => { diff --git a/src/config.ts b/src/config.ts index 742963376..bbb139a61 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,21 +26,7 @@ export type ConfigAliasOptions = { defaultHidden: boolean; }; -export type HostComponentNames = { - text: string; - textInput: string; - image: string; - switch: string; - scrollView: string; - modal: string; -}; - -export type InternalConfig = Config & { - /** Names for key React Native host components. */ - hostComponentNames?: HostComponentNames; -}; - -const defaultConfig: InternalConfig = { +const defaultConfig: Config = { asyncUtilTimeout: 1000, defaultIncludeHiddenElements: false, concurrentRoot: true, diff --git a/src/fire-event.ts b/src/fire-event.ts index 0f0287f5e..fe3fe6d79 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -52,7 +52,7 @@ export function isEventEnabled( eventName: string, nearestTouchResponder?: ReactTestInstance, ) { - if (isHostTextInput(nearestTouchResponder)) { + if (nearestTouchResponder != null && isHostTextInput(nearestTouchResponder)) { return ( isTextInputEditable(nearestTouchResponder) || textInputEventsIgnoringEditableProp.has(eventName) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 5eee9401a..062ea8fb2 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -7,13 +7,7 @@ import { } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; import { getHostSiblings, getUnsafeRootElement } from './component-tree'; -import { - getHostComponentNames, - isHostImage, - isHostSwitch, - isHostText, - isHostTextInput, -} from './host-component-names'; +import { isHostImage, isHostSwitch, isHostText, isHostTextInput } from './host-component-names'; import { getTextContent } from './text-content'; import { isTextInputEditable } from './text-input'; @@ -112,12 +106,7 @@ export function isAccessibilityElement(element: ReactTestInstance | null): boole return element.props.accessible; } - const hostComponentNames = getHostComponentNames(); - return ( - element?.type === hostComponentNames?.text || - element?.type === hostComponentNames?.textInput || - element?.type === hostComponentNames?.switch - ); + return isHostText(element) || isHostTextInput(element) || isHostSwitch(element); } /** diff --git a/src/helpers/host-component-names.ts b/src/helpers/host-component-names.ts new file mode 100644 index 000000000..c960bf264 --- /dev/null +++ b/src/helpers/host-component-names.ts @@ -0,0 +1,57 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import { HostTestInstance } from './component-tree'; + +const HOST_TEXT_NAMES = ['Text', 'RCTText']; +const HOST_TEXT_INPUT_NAMES = ['TextInput']; +const HOST_IMAGE_NAMES = ['Image']; +const HOST_SWITCH_NAMES = ['RCTSwitch']; +const HOST_SCROLL_VIEW_NAMES = ['RCTScrollView']; +const HOST_MODAL_NAMES = ['Modal']; + +/** + * Checks if the given element is a host Text element. + * @param element The element to check. + */ +export function isHostText(element: ReactTestInstance): element is HostTestInstance { + return typeof element?.type === 'string' && HOST_TEXT_NAMES.includes(element.type); +} + +/** + * Checks if the given element is a host TextInput element. + * @param element The element to check. + */ +export function isHostTextInput(element: ReactTestInstance): element is HostTestInstance { + return typeof element?.type === 'string' && HOST_TEXT_INPUT_NAMES.includes(element.type); +} + +/** + * Checks if the given element is a host Image element. + * @param element The element to check. + */ +export function isHostImage(element: ReactTestInstance): element is HostTestInstance { + return typeof element?.type === 'string' && HOST_IMAGE_NAMES.includes(element.type); +} + +/** + * Checks if the given element is a host Switch element. + * @param element The element to check. + */ +export function isHostSwitch(element: ReactTestInstance): element is HostTestInstance { + return typeof element?.type === 'string' && HOST_SWITCH_NAMES.includes(element.type); +} + +/** + * Checks if the given element is a host ScrollView element. + * @param element The element to check. + */ +export function isHostScrollView(element: ReactTestInstance): element is HostTestInstance { + return typeof element?.type === 'string' && HOST_SCROLL_VIEW_NAMES.includes(element.type); +} + +/** + * Checks if the given element is a host Modal element. + * @param element The element to check. + */ +export function isHostModal(element: ReactTestInstance): element is HostTestInstance { + return typeof element?.type === 'string' && HOST_MODAL_NAMES.includes(element.type); +} diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx deleted file mode 100644 index b450c930b..000000000 --- a/src/helpers/host-component-names.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import * as React from 'react'; -import { ReactTestInstance } from 'react-test-renderer'; -import { Image, Modal, ScrollView, 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.`; - -export function getHostComponentNames(): HostComponentNames { - let hostComponentNames = getConfig().hostComponentNames; - if (!hostComponentNames) { - hostComponentNames = detectHostComponentNames(); - configureInternal({ hostComponentNames }); - } - - return hostComponentNames; -} - -export function configureHostComponentNamesIfNeeded() { - const configHostComponentNames = getConfig().hostComponentNames; - if (configHostComponentNames) { - return; - } - - const hostComponentNames = detectHostComponentNames(); - configureInternal({ hostComponentNames }); -} - -function detectHostComponentNames(): HostComponentNames { - try { - const renderer = renderWithAct( - - Hello - - - - - - , - ); - - return { - text: getByTestId(renderer.root, 'text').type as string, - textInput: getByTestId(renderer.root, 'textInput').type as string, - image: getByTestId(renderer.root, 'image').type as string, - switch: getByTestId(renderer.root, 'switch').type as string, - scrollView: getByTestId(renderer.root, 'scrollView').type as string, - modal: getByTestId(renderer.root, 'modal').type as string, - }; - } 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${userConfigErrorMessage}`, - ); - } -} - -function getByTestId(instance: ReactTestInstance, testID: string) { - const nodes = instance.findAll( - (node) => typeof node.type === 'string' && node.props.testID === testID, - ); - - if (nodes.length === 0) { - throw new Error(`Unable to find an element with testID: ${testID}`); - } - - return nodes[0]; -} - -/** - * Checks if the given element is a host Text element. - * @param element The element to check. - */ -export function isHostText(element?: ReactTestInstance | null): element is HostTestInstance { - return element?.type === getHostComponentNames().text; -} - -/** - * Checks if the given element is a host TextInput element. - * @param element The element to check. - */ -export function isHostTextInput(element?: ReactTestInstance | null): element is HostTestInstance { - return element?.type === getHostComponentNames().textInput; -} - -/** - * Checks if the given element is a host Image element. - * @param element The element to check. - */ -export function isHostImage(element?: ReactTestInstance | null): element is HostTestInstance { - return element?.type === getHostComponentNames().image; -} - -/** - * Checks if the given element is a host Switch element. - * @param element The element to check. - */ -export function isHostSwitch(element?: ReactTestInstance | null): element is HostTestInstance { - return element?.type === getHostComponentNames().switch; -} - -/** - * Checks if the given element is a host ScrollView element. - * @param element The element to check. - */ -export function isHostScrollView(element?: ReactTestInstance | null): element is HostTestInstance { - return element?.type === getHostComponentNames().scrollView; -} - -/** - * Checks if the given element is a host Modal element. - * @param element The element to check. - */ -export function isHostModal(element?: ReactTestInstance | null): element is HostTestInstance { - return element?.type === getHostComponentNames().modal; -} diff --git a/src/render-hook.tsx b/src/render-hook.tsx index f6e7cf08b..ba30077b0 100644 --- a/src/render-hook.tsx +++ b/src/render-hook.tsx @@ -37,7 +37,6 @@ export function renderHook( , { wrapper, - detectHostComponentNames: false, }, ); diff --git a/src/render.tsx b/src/render.tsx index 7727130c2..dfbec3155 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -8,9 +8,8 @@ import { Profiler } from 'react'; import act from './act'; import { addToCleanupQueue } from './cleanup'; import { getConfig } from './config'; -import { getHostChildren } from './helpers/component-tree'; +import { getHostSelves } from './helpers/component-tree'; import { debug, DebugOptions } from './helpers/debug'; -import { configureHostComponentNamesIfNeeded } from './helpers/host-component-names'; import { validateStringsRenderedWithinText } from './helpers/string-validation'; import { renderWithAct } from './render-act'; import { setRenderResult } from './screen'; @@ -43,18 +42,10 @@ export default function render(component: React.ReactElement, options: Ren return renderInternal(component, options); } -export interface RenderInternalOptions extends RenderOptions { - detectHostComponentNames?: boolean; -} - -export function renderInternal( - component: React.ReactElement, - options?: RenderInternalOptions, -) { +export function renderInternal(component: React.ReactElement, options?: RenderOptions) { const { wrapper: Wrapper, concurrentRoot, - detectHostComponentNames = true, unstable_validateStringsRenderedWithinText, ...rest } = options || {}; @@ -65,10 +56,6 @@ export function renderInternal( unstable_isConcurrent: concurrentRoot ?? getConfig().concurrentRoot, }; - if (detectHostComponentNames) { - configureHostComponentNamesIfNeeded(); - } - if (unstable_validateStringsRenderedWithinText) { return renderWithStringValidation(component, { wrapper: Wrapper, @@ -130,7 +117,7 @@ function buildRenderResult( toJSON: renderer.toJSON, debug: makeDebug(instance, renderer), get root(): ReactTestInstance { - return getHostChildren(instance)[0]; + return getHostSelves(instance)[0]; }, UNSAFE_root: instance, }; From 508ad32262af2aa756358437790ded18aee0c8b8 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 31 Oct 2024 10:10:58 +0100 Subject: [PATCH 2/2] chore: fix lint --- src/__tests__/config.test.ts | 4 ++-- src/config.ts | 7 ------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 50b955675..803cfd621 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -1,4 +1,4 @@ -import { getConfig, configure, resetToDefaults, configureInternal } from '../config'; +import { getConfig, configure, resetToDefaults } from '../config'; beforeEach(() => { resetToDefaults(); @@ -34,7 +34,7 @@ test('resetToDefaults() resets config to defaults', () => { }); test('resetToDefaults() resets internal config to defaults', () => { - configureInternal({ asyncUtilTimeout: 2000 }); + configure({ asyncUtilTimeout: 2000 }); expect(getConfig().asyncUtilTimeout).toBe(2000); resetToDefaults(); diff --git a/src/config.ts b/src/config.ts index bbb139a61..7d5d74617 100644 --- a/src/config.ts +++ b/src/config.ts @@ -52,13 +52,6 @@ export function configure(options: Partial) { }; } -export function configureInternal(option: Partial) { - config = { - ...config, - ...option, - }; -} - export function resetToDefaults() { config = { ...defaultConfig }; }