From fc9ece9fa3d815d2b5e54095c1ef8897d73ac99e Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Fri, 3 Feb 2023 14:15:59 +0100 Subject: [PATCH 1/5] fix: run autodetection on render to prevent detection to be wrapped by act --- src/__tests__/host-component-names.test.tsx | 11 +++++++++++ src/helpers/host-component-names.tsx | 6 ++++-- src/render.tsx | 3 +++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 0a1fd203b..6384499a8 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -1,8 +1,10 @@ +import React from 'react'; import { View } from 'react-native'; import TestRenderer from 'react-test-renderer'; import { configureInternal, getConfig } from '../config'; import { getHostComponentNames } from '../helpers/host-component-names'; import * as within from '../within'; +import { act, render } from '..'; const mockCreate = jest.spyOn(TestRenderer, 'create') as jest.Mock; const mockGetQueriesForElements = jest.spyOn( @@ -35,6 +37,15 @@ describe('getHostComponentNames', () => { }); }); + test('does not throw when wrapped in act after render has been called', () => { + render(); + expect(() => + act(() => { + getHostComponentNames(); + }) + ).not.toThrow(); + }); + test('throw an error when autodetection fails', () => { mockCreate.mockReturnValue({ root: { type: View, children: [], props: {} }, diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx index 5872c2417..d4a188dfe 100644 --- a/src/helpers/host-component-names.tsx +++ b/src/helpers/host-component-names.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Text, TextInput, View } from 'react-native'; import TestRenderer from 'react-test-renderer'; +import type { ReactTestRenderer } 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.`; @@ -12,14 +12,16 @@ export function getHostComponentNames(): HostComponentNames { if (configHostComponentNames) { return configHostComponentNames; } + let renderer: ReactTestRenderer; try { - const renderer = TestRenderer.create( + renderer = TestRenderer.create( Hello ); + const { getByTestId } = getQueriesForElement(renderer.root); const textHostName = getByTestId('text').type; const textInputHostName = getByTestId('textInput').type; diff --git a/src/render.tsx b/src/render.tsx index a25828985..44c6f07d4 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -10,6 +10,7 @@ import { getQueriesForElement } from './within'; import { setRenderResult, screen } from './screen'; import { validateStringsRenderedWithinText } from './helpers/stringValidation'; import { getConfig } from './config'; +import { getHostComponentNames } from './helpers/host-component-names'; export type RenderOptions = { wrapper?: React.ComponentType; @@ -42,6 +43,8 @@ export default function render( }); } + getHostComponentNames(); + const wrap = (element: React.ReactElement) => Wrapper ? {element} : element; From 65b5eb6705bc30ac9e6241b6cae08d3addb32b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 3 Feb 2023 23:05:34 +0000 Subject: [PATCH 2/5] refactor: split getHostComponentNames --- src/__tests__/host-component-names.test.tsx | 61 +++++++++++++++------ src/helpers/host-component-names.tsx | 29 +++++++--- src/render.tsx | 6 +- 3 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 6384499a8..9a6eedc39 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -2,7 +2,10 @@ import React from 'react'; import { View } from 'react-native'; import TestRenderer from 'react-test-renderer'; import { configureInternal, getConfig } from '../config'; -import { getHostComponentNames } from '../helpers/host-component-names'; +import { + getHostComponentNames, + configureHostComponentNamesIfNeeded, +} from '../helpers/host-component-names'; import * as within from '../within'; import { act, render } from '..'; @@ -13,10 +16,41 @@ const mockGetQueriesForElements = jest.spyOn( ) as jest.Mock; describe('getHostComponentNames', () => { + test('returns host component names from internal config', () => { + configureInternal({ + hostComponentNames: { text: 'banana', textInput: 'banana' }, + }); + + expect(getHostComponentNames()).toEqual({ + text: 'banana', + textInput: 'banana', + }); + }); + + test('throws when names are missing in the internal config', () => { + expect(() => getHostComponentNames()).toThrowErrorMatchingInlineSnapshot(` + "Missing host component names. + + 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('does not throw when wrapped in act after render has been called', () => { + render(); + expect(() => + act(() => { + getHostComponentNames(); + }) + ).not.toThrow(); + }); +}); + +describe('configureHostComponentNamesIfNeeded', () => { test('updates internal config with host component names when they are not defined', () => { expect(getConfig().hostComponentNames).toBeUndefined(); - getHostComponentNames(); + configureHostComponentNamesIfNeeded(); expect(getConfig().hostComponentNames).toEqual({ text: 'Text', @@ -29,7 +63,7 @@ describe('getHostComponentNames', () => { hostComponentNames: { text: 'banana', textInput: 'banana' }, }); - getHostComponentNames(); + configureHostComponentNamesIfNeeded(); expect(getConfig().hostComponentNames).toEqual({ text: 'banana', @@ -37,28 +71,19 @@ describe('getHostComponentNames', () => { }); }); - test('does not throw when wrapped in act after render has been called', () => { - render(); - expect(() => - act(() => { - getHostComponentNames(); - }) - ).not.toThrow(); - }); - test('throw an error when autodetection fails', () => { mockCreate.mockReturnValue({ root: { type: View, children: [], props: {} }, }); - expect(() => getHostComponentNames()).toThrowErrorMatchingInlineSnapshot(` + expect(() => configureHostComponentNamesIfNeeded()) + .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. - " + Please check if you are using compatible versions of React Native and React Native Testing Library." `); }); @@ -69,14 +94,14 @@ describe('getHostComponentNames', () => { }, }); - expect(() => getHostComponentNames()).toThrowErrorMatchingInlineSnapshot(` + expect(() => configureHostComponentNamesIfNeeded()) + .toThrowErrorMatchingInlineSnapshot(` "Trying to detect host component names triggered the following error: getByTestId returned non-host component 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. - " + Please check if you are using compatible versions of React Native and React Native Testing Library." `); }); }); diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx index d4a188dfe..051cae64d 100644 --- a/src/helpers/host-component-names.tsx +++ b/src/helpers/host-component-names.tsx @@ -1,21 +1,33 @@ import React from 'react'; import { Text, TextInput, View } from 'react-native'; import TestRenderer from 'react-test-renderer'; -import type { ReactTestRenderer } 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) { + throw new Error(`Missing host component names.\n\n${defaultErrorMessage}`); + } + + return configHostComponentNames; +} + +export function configureHostComponentNamesIfNeeded() { const configHostComponentNames = getConfig().hostComponentNames; if (configHostComponentNames) { - return configHostComponentNames; + return; } - let renderer: ReactTestRenderer; + const hostComponentNames = detectHostComponentNames(); + configureInternal({ hostComponentNames }); +} + +function detectHostComponentNames(): HostComponentNames { try { - renderer = TestRenderer.create( + const renderer = TestRenderer.create( Hello @@ -34,19 +46,18 @@ export function getHostComponentNames(): HostComponentNames { throw new Error('getByTestId returned non-host component'); } - const hostComponentNames = { + return { 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} -`); + throw new Error( + `Trying to detect host component names triggered the following error:\n\n${errorMessage}\n\n${defaultErrorMessage}` + ); } } diff --git a/src/render.tsx b/src/render.tsx index 44c6f07d4..b60d15139 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -10,7 +10,7 @@ import { getQueriesForElement } from './within'; import { setRenderResult, screen } from './screen'; import { validateStringsRenderedWithinText } from './helpers/stringValidation'; import { getConfig } from './config'; -import { getHostComponentNames } from './helpers/host-component-names'; +import { configureHostComponentNamesIfNeeded } from './helpers/host-component-names'; export type RenderOptions = { wrapper?: React.ComponentType; @@ -36,6 +36,8 @@ export default function render( unstable_validateStringsRenderedWithinText, }: RenderOptions = {} ) { + configureHostComponentNamesIfNeeded(); + if (unstable_validateStringsRenderedWithinText) { return renderWithStringValidation(component, { wrapper: Wrapper, @@ -43,8 +45,6 @@ export default function render( }); } - getHostComponentNames(); - const wrap = (element: React.ReactElement) => Wrapper ? {element} : element; From 86a3434b69fbfe4e787a637ddb395145bf0c7786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 3 Feb 2023 23:11:25 +0000 Subject: [PATCH 3/5] chore: tweaks --- src/__tests__/host-component-names.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 9a6eedc39..aa2558b9d 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { View } from 'react-native'; import TestRenderer from 'react-test-renderer'; import { configureInternal, getConfig } from '../config'; From bc5897587b3b444f2314603ec34d035ad681b1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 8 Feb 2023 10:44:11 +0100 Subject: [PATCH 4/5] chore: code review changes --- src/__tests__/host-component-names.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index aa2558b9d..40761a9d8 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -36,6 +36,10 @@ describe('getHostComponentNames', () => { `); }); + // 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(() => From 47c850659d17831ab0afd16da63af47a330530dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 8 Feb 2023 10:56:17 +0100 Subject: [PATCH 5/5] refactor: simplify getHostComponentNames by adding lazy detect --- src/__tests__/host-component-names.test.tsx | 15 +++++++++------ src/helpers/host-component-names.tsx | 14 ++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 40761a9d8..8d10c658d 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -27,13 +27,16 @@ describe('getHostComponentNames', () => { }); }); - test('throws when names are missing in the internal config', () => { - expect(() => getHostComponentNames()).toThrowErrorMatchingInlineSnapshot(` - "Missing host component names. + test('detects host component names if not present in internal config', () => { + expect(getConfig().hostComponentNames).toBeUndefined(); - 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." - `); + const hostComponentNames = getHostComponentNames(); + + expect(hostComponentNames).toEqual({ + text: 'Text', + textInput: 'TextInput', + }); + expect(getConfig().hostComponentNames).toBe(hostComponentNames); }); // Repro test for case when user indirectly triggers `getHostComponentNames` calls from diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx index 051cae64d..b6b06a008 100644 --- a/src/helpers/host-component-names.tsx +++ b/src/helpers/host-component-names.tsx @@ -3,16 +3,18 @@ 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. + +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 { - const configHostComponentNames = getConfig().hostComponentNames; - if (!configHostComponentNames) { - throw new Error(`Missing host component names.\n\n${defaultErrorMessage}`); + let hostComponentNames = getConfig().hostComponentNames; + if (!hostComponentNames) { + hostComponentNames = detectHostComponentNames(); + configureInternal({ hostComponentNames }); } - return configHostComponentNames; + return hostComponentNames; } export function configureHostComponentNamesIfNeeded() { @@ -57,7 +59,7 @@ function detectHostComponentNames(): HostComponentNames { : null; throw new Error( - `Trying to detect host component names triggered the following error:\n\n${errorMessage}\n\n${defaultErrorMessage}` + `Trying to detect host component names triggered the following error:\n\n${errorMessage}\n\n${userConfigErrorMessage}` ); } }