From 55f8026917382b3cf76f176367deb474e8f1b6a7 Mon Sep 17 00:00:00 2001 From: Stephen Hanson Date: Fri, 31 Mar 2023 15:03:09 -0500 Subject: [PATCH 01/24] feat: Render virtual DOM in byText error message --- src/queries/__tests__/text.test.tsx | 20 ++++++++++++++++---- src/queries/text.ts | 6 +++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index 0f42daff7..0e2a889e2 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -496,13 +496,25 @@ test('byText support hidden option', () => { expect(queryByText(/hidden/i)).toBeFalsy(); expect(queryByText(/hidden/i, { includeHiddenElements: false })).toBeFalsy(); - expect(() => - getByText(/hidden/i, { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with text: /hidden/i"` + expect(() => getByText(/hidden/i, { includeHiddenElements: false })).toThrow( + /Unable to find an element with text: \/hidden\/i/ ); }); +test('byText renders the React DOM without props on failure', async () => { + const { getByText } = render( + Some text + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + +  + Some text + " +`); +}); + test('byText should return host component', () => { const { getByText } = render(hello); expect(getByText('hello').type).toBe('Text'); diff --git a/src/queries/text.ts b/src/queries/text.ts index 425f0153c..15277af9e 100644 --- a/src/queries/text.ts +++ b/src/queries/text.ts @@ -4,6 +4,8 @@ import { findAll } from '../helpers/findAll'; import { getHostComponentNames } from '../helpers/host-component-names'; import { matchTextContent } from '../helpers/matchers/matchTextContent'; import { TextMatch, TextMatchOptions } from '../matches'; +import format from '../helpers/format'; +import { screen } from '../screen'; import { makeQueries } from './makeQueries'; import type { FindAllByQuery, @@ -37,7 +39,9 @@ const getMultipleError = (text: TextMatch) => `Found multiple elements with text: ${String(text)}`; const getMissingError = (text: TextMatch) => - `Unable to find an element with text: ${String(text)}`; + `Unable to find an element with text: ${String(text)} + +${format(screen.toJSON() || [], { mapProps: () => ({}) })}`; const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( queryAllByText, From 1f64a25c51e8d4f6eaa276d54646ab5ae61e04cb Mon Sep 17 00:00:00 2001 From: Stephen Hanson Date: Fri, 31 Mar 2023 16:15:13 -0500 Subject: [PATCH 02/24] Preserve props that hide, since relevant to failure --- src/__tests__/mapProps.test.tsx | 41 ++++++++++ src/helpers/format.ts | 2 +- src/helpers/mapProps.ts | 26 +++++++ src/queries/__tests__/text.test.tsx | 113 +++++++++++++++++++++++++--- src/queries/text.ts | 7 +- 5 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 src/__tests__/mapProps.test.tsx create mode 100644 src/helpers/mapProps.ts diff --git a/src/__tests__/mapProps.test.tsx b/src/__tests__/mapProps.test.tsx new file mode 100644 index 000000000..2b645fff8 --- /dev/null +++ b/src/__tests__/mapProps.test.tsx @@ -0,0 +1,41 @@ +import { ReactTestRendererJSON } from 'react-test-renderer'; +import { mapVisibilityRelatedProps } from '../helpers/mapProps'; + +const node: ReactTestRendererJSON = { + type: 'View', + props: {}, + children: null, +}; + +describe('mapVisibilityRelatedProps', () => { + test('preserves props that hide an element', () => { + const result = mapVisibilityRelatedProps( + { + accessibilityElementsHidden: true, + importantForAccessibility: 'no-hide-descendants', + style: [{ flex: 1 }, { borderWidth: 2, display: 'none' }], + }, + node + ); + + expect(result).toEqual({ + accessibilityElementsHidden: true, + importantForAccessibility: 'no-hide-descendants', + style: { display: 'none' }, + }); + }); + + test('does not preserve props that do not hide', () => { + const result = mapVisibilityRelatedProps( + { + accessibilityElementsHidden: false, + importantForAccessibility: 'auto', + style: [{ flex: 1 }, { display: 'flex' }], + testID: 'my-component', + }, + node + ); + + expect(result).toEqual({}); + }); +}); diff --git a/src/helpers/format.ts b/src/helpers/format.ts index f8dbf6d3e..a24730968 100644 --- a/src/helpers/format.ts +++ b/src/helpers/format.ts @@ -1,7 +1,7 @@ import type { ReactTestRendererJSON } from 'react-test-renderer'; import prettyFormat, { NewPlugin, plugins } from 'pretty-format'; -type MapPropsFunction = ( +export type MapPropsFunction = ( props: Record, node: ReactTestRendererJSON ) => Record; diff --git a/src/helpers/mapProps.ts b/src/helpers/mapProps.ts new file mode 100644 index 000000000..7c6ac7600 --- /dev/null +++ b/src/helpers/mapProps.ts @@ -0,0 +1,26 @@ +import { StyleSheet } from 'react-native'; +import { MapPropsFunction } from './format'; + +/** + * Preserve props that would cause an element to not be visible while stripping + * the rest + */ +export const mapVisibilityRelatedProps: MapPropsFunction = ({ + accessibilityElementsHidden, + accessibilityViewIsModal, + importantForAccessibility, + style, +}) => { + const styles = StyleSheet.flatten(style as any) ?? {}; + + return { + ...(accessibilityElementsHidden + ? { accessibilityElementsHidden } + : undefined), + ...(accessibilityViewIsModal ? { accessibilityViewIsModal } : undefined), + ...(importantForAccessibility === 'no-hide-descendants' + ? { importantForAccessibility } + : undefined), + ...(styles.display === 'none' ? { style: { display: 'none' } } : undefined), + }; +}; diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index 0e2a889e2..1d432eac8 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -501,18 +501,111 @@ test('byText support hidden option', () => { ); }); -test('byText renders the React DOM without props on failure', async () => { - const { getByText } = render( - Some text - ); +describe('byText error message', () => { + test('renders the React DOM with regular props stripped', async () => { + const { getByText } = render( + Some text + ); - expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with text: /foo/ + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ -  - Some text - " -`); +  + Some text + " + `); + }); + + test('does not strip accessibilityElementsHidden prop if true', () => { + const { getByText } = render( + + Some text + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + +  +  + Some text +  + " + `); + }); + + test('does not strip accessibilityViewIsModal prop if true', () => { + const { getByText } = render( + + Some text + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + +  +  + Some text +  + " + `); + }); + + test('does not strip importantForAccessibility prop if "no-hide-descendants"', () => { + const { getByText } = render( + + Some text + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + +  +  + Some text +  + " + `); + }); + + test('does not strip display: none from "style" prop, but does strip other styles', () => { + const { getByText } = render( + + + Some text + + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + +  +  + Some text +  + " + `); + }); }); test('byText should return host component', () => { diff --git a/src/queries/text.ts b/src/queries/text.ts index 15277af9e..e83ec4037 100644 --- a/src/queries/text.ts +++ b/src/queries/text.ts @@ -1,12 +1,12 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { filterNodeByType } from '../helpers/filterNodeByType'; import { findAll } from '../helpers/findAll'; +import format from '../helpers/format'; import { getHostComponentNames } from '../helpers/host-component-names'; +import { mapVisibilityRelatedProps } from '../helpers/mapProps'; import { matchTextContent } from '../helpers/matchers/matchTextContent'; import { TextMatch, TextMatchOptions } from '../matches'; -import format from '../helpers/format'; import { screen } from '../screen'; -import { makeQueries } from './makeQueries'; import type { FindAllByQuery, FindByQuery, @@ -15,6 +15,7 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; +import { makeQueries } from './makeQueries'; import type { CommonQueryOptions } from './options'; type ByTextOptions = CommonQueryOptions & TextMatchOptions; @@ -41,7 +42,7 @@ const getMultipleError = (text: TextMatch) => const getMissingError = (text: TextMatch) => `Unable to find an element with text: ${String(text)} -${format(screen.toJSON() || [], { mapProps: () => ({}) })}`; +${format(screen.toJSON() || [], { mapProps: mapVisibilityRelatedProps })}`; const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( queryAllByText, From e960d9153d2805e4289b63fd11663af0d9ef8d5d Mon Sep 17 00:00:00 2001 From: Stephen Hanson Date: Tue, 4 Apr 2023 17:17:21 -0500 Subject: [PATCH 03/24] Refactor to add props, move logic to makeQueries Todo: * Add more tests to mapProps.test * Add tests for other queries * Potentially optimize for findBy queries --- src/__tests__/mapProps.test.tsx | 51 +++++++++-- src/helpers/mapProps.ts | 81 +++++++++++++---- src/queries/__tests__/text.test.tsx | 134 +++++++++++++++++++--------- src/queries/makeQueries.ts | 26 +++++- src/queries/text.ts | 7 +- 5 files changed, 218 insertions(+), 81 deletions(-) diff --git a/src/__tests__/mapProps.test.tsx b/src/__tests__/mapProps.test.tsx index 2b645fff8..c3d03ffd1 100644 --- a/src/__tests__/mapProps.test.tsx +++ b/src/__tests__/mapProps.test.tsx @@ -1,5 +1,5 @@ import { ReactTestRendererJSON } from 'react-test-renderer'; -import { mapVisibilityRelatedProps } from '../helpers/mapProps'; +import { mapPropsForQueryError } from '../helpers/mapProps'; const node: ReactTestRendererJSON = { type: 'View', @@ -7,9 +7,9 @@ const node: ReactTestRendererJSON = { children: null, }; -describe('mapVisibilityRelatedProps', () => { +describe('mapPropsForQueryError', () => { test('preserves props that hide an element', () => { - const result = mapVisibilityRelatedProps( + const result = mapPropsForQueryError( { accessibilityElementsHidden: true, importantForAccessibility: 'no-hide-descendants', @@ -18,24 +18,57 @@ describe('mapVisibilityRelatedProps', () => { node ); - expect(result).toEqual({ + expect(result).toStrictEqual({ accessibilityElementsHidden: true, importantForAccessibility: 'no-hide-descendants', style: { display: 'none' }, }); }); + test('removes undefined keys from accessibilityState', () => { + const result = mapPropsForQueryError( + { + accessibilityState: { checked: undefined, selected: true }, + }, + node + ); + + expect(result).toStrictEqual({ + accessibilityState: { selected: true }, + }); + }); + + test('does not fail if accessibilityState is a string, passes through', () => { + const result = mapPropsForQueryError({ accessibilityState: 'foo' }, node); + expect(result).toStrictEqual({ accessibilityState: 'foo' }); + }); + + test('does not fail if accessibilityState is an array, passes through', () => { + const result = mapPropsForQueryError({ accessibilityState: [1] }, node); + expect(result).toStrictEqual({ accessibilityState: [1] }); + }); + + test('does not fail if accessibilityState is null, passes through', () => { + const result = mapPropsForQueryError({ accessibilityState: null }, node); + expect(result).toStrictEqual({ accessibilityState: null }); + }); + + test('does not fail if accessibilityState is nested object, passes through', () => { + const accessibilityState = { 1: { 2: 3 }, 2: undefined }; + const result = mapPropsForQueryError({ accessibilityState }, node); + expect(result).toStrictEqual({ accessibilityState: { 1: { 2: 3 } } }); + }); + test('does not preserve props that do not hide', () => { - const result = mapVisibilityRelatedProps( + const result = mapPropsForQueryError( { - accessibilityElementsHidden: false, - importantForAccessibility: 'auto', style: [{ flex: 1 }, { display: 'flex' }], - testID: 'my-component', + onPress: () => null, + key: 'foo', }, node ); - expect(result).toEqual({}); + expect(result).toStrictEqual({}); }); }); diff --git a/src/helpers/mapProps.ts b/src/helpers/mapProps.ts index 7c6ac7600..5d19dad1c 100644 --- a/src/helpers/mapProps.ts +++ b/src/helpers/mapProps.ts @@ -1,26 +1,71 @@ import { StyleSheet } from 'react-native'; import { MapPropsFunction } from './format'; +const propsToDisplay = [ + 'accessibilityElementsHidden', + 'accessibilityViewIsModal', + 'importantForAccessibility', + 'testID', + 'nativeID', + 'accessibilityLabel', + 'accessibilityLabelledBy', + 'accessibilityRole', + 'accessibilityHint', + 'placeholder', + 'value', + 'defaultValue', +]; + +function isObject( + thing: unknown +): thing is Record { + return typeof thing === 'object' && !Array.isArray(thing) && thing !== null; +} + +function removeEmptyKeys(prop: unknown) { + if (isObject(prop)) { + const object = Object.keys(prop).reduce((acc, propName) => { + return { + ...acc, + ...(prop[propName] === undefined ? {} : { [propName]: prop[propName] }), + }; + }, {}); + + if (!Object.values(object).find((val) => val !== undefined)) { + return undefined; + } + + return object; + } + + return prop; +} + /** - * Preserve props that would cause an element to not be visible while stripping - * the rest + * Preserve props that are helpful in diagnosing test failures, while stripping rest */ -export const mapVisibilityRelatedProps: MapPropsFunction = ({ - accessibilityElementsHidden, - accessibilityViewIsModal, - importantForAccessibility, - style, -}) => { - const styles = StyleSheet.flatten(style as any) ?? {}; - - return { - ...(accessibilityElementsHidden - ? { accessibilityElementsHidden } - : undefined), - ...(accessibilityViewIsModal ? { accessibilityViewIsModal } : undefined), - ...(importantForAccessibility === 'no-hide-descendants' - ? { importantForAccessibility } - : undefined), +export const mapPropsForQueryError: MapPropsFunction = (props) => { + const accessibilityState = removeEmptyKeys(props.accessibilityState); + const accessibilityValue = removeEmptyKeys(props.accessibilityValue); + + const styles = StyleSheet.flatten(props.style as any) ?? {}; + + // perform custom prop mappings + const mappedProps: Record = { ...(styles.display === 'none' ? { style: { display: 'none' } } : undefined), + ...(accessibilityState === undefined ? {} : { accessibilityState }), + ...(accessibilityValue === undefined ? {} : { accessibilityValue }), }; + + // add props from propsToDisplay without mapping + return propsToDisplay.reduce((acc, propName) => { + if (propName in props) { + return { + ...acc, + [propName]: props[propName], + }; + } + + return acc; + }, mappedProps); }; diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index 1d432eac8..390529860 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -503,9 +503,7 @@ test('byText support hidden option', () => { describe('byText error message', () => { test('renders the React DOM with regular props stripped', async () => { - const { getByText } = render( - Some text - ); + const { getByText } = render( null}>Some text); expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ @@ -516,63 +514,66 @@ describe('byText error message', () => { `); }); - test('does not strip accessibilityElementsHidden prop if true', () => { + test('passes through helpful props', async () => { const { getByText } = render( - - Some text - - ); - - expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with text: /foo/ - -  -  - Some text -  - " - `); - }); - - test('does not strip accessibilityViewIsModal prop if true', () => { - const { getByText } = render( - - Some text + + + Some Text ); expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ -  -  - Some text +  +  +  + Some Text  " `); }); - test('does not strip importantForAccessibility prop if "no-hide-descendants"', () => { - const { getByText } = render( - - Some text - + test('works with findBy', async () => { + const { findByText } = render( + ); - expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + await expect(() => findByText(/foo/)).rejects + .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ -  -  - Some text -  - " + " `); }); @@ -606,6 +607,51 @@ describe('byText error message', () => { " `); }); + + test('strips undefined values from accessibilityState', () => { + const { getByText } = render( + + + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + +  +  + " + `); + }); + + test('strips undefined values from accessibilityValue', () => { + const { getByText } = render( + + + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + +  +  + " + `); + }); }); test('byText should return host component', () => { diff --git a/src/queries/makeQueries.ts b/src/queries/makeQueries.ts index a309d10fa..792ad2136 100644 --- a/src/queries/makeQueries.ts +++ b/src/queries/makeQueries.ts @@ -2,6 +2,9 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { ErrorWithStack } from '../helpers/errors'; import waitFor from '../waitFor'; import type { WaitForOptions } from '../waitFor'; +import format from '../helpers/format'; +import { screen } from '../screen'; +import { mapPropsForQueryError } from '../helpers/mapProps'; export type GetByQuery = ( predicate: Predicate, @@ -72,8 +75,8 @@ function extractDeprecatedWaitForOptions(options?: WaitForOptions) { if (option) { // eslint-disable-next-line no-console console.warn( - `Use of option "${key}" in a findBy* query options (2nd parameter) is deprecated. Please pass this option in the waitForOptions (3rd parameter). -Example: + `Use of option "${key}" in a findBy* query options (2nd parameter) is deprecated. Please pass this option in the waitForOptions (3rd parameter). +Example: findByText(text, {}, { ${key}: ${option.toString()} })` ); @@ -83,6 +86,15 @@ Example: return waitForOptions; } +/** + * @returns formatted DOM with two newlines preceding + */ +function getFormattedDOM() { + return ` + +${format(screen.toJSON() || [], { mapProps: mapPropsForQueryError })}`; +} + export function makeQueries( queryAllByQuery: UnboundQuery>, getMissingError: (predicate: Predicate, options?: Options) => string, @@ -93,7 +105,10 @@ export function makeQueries( const results = queryAllByQuery(instance)(predicate, options); if (results.length === 0) { - throw new ErrorWithStack(getMissingError(predicate, options), getAllFn); + throw new ErrorWithStack( + `${getMissingError(predicate, options)}${getFormattedDOM()}`, + getAllFn + ); } return results; @@ -128,7 +143,10 @@ export function makeQueries( } if (results.length === 0) { - throw new ErrorWithStack(getMissingError(predicate, options), getFn); + throw new ErrorWithStack( + `${getMissingError(predicate, options)}${getFormattedDOM()}`, + getFn + ); } return results[0]; diff --git a/src/queries/text.ts b/src/queries/text.ts index e83ec4037..b687e2f3b 100644 --- a/src/queries/text.ts +++ b/src/queries/text.ts @@ -1,12 +1,9 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { filterNodeByType } from '../helpers/filterNodeByType'; import { findAll } from '../helpers/findAll'; -import format from '../helpers/format'; import { getHostComponentNames } from '../helpers/host-component-names'; -import { mapVisibilityRelatedProps } from '../helpers/mapProps'; import { matchTextContent } from '../helpers/matchers/matchTextContent'; import { TextMatch, TextMatchOptions } from '../matches'; -import { screen } from '../screen'; import type { FindAllByQuery, FindByQuery, @@ -40,9 +37,7 @@ const getMultipleError = (text: TextMatch) => `Found multiple elements with text: ${String(text)}`; const getMissingError = (text: TextMatch) => - `Unable to find an element with text: ${String(text)} - -${format(screen.toJSON() || [], { mapProps: mapVisibilityRelatedProps })}`; + `Unable to find an element with text: ${String(text)}`; const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( queryAllByText, From fb9e89178e2f1cfc9faf63711606380f2af0eb9b Mon Sep 17 00:00:00 2001 From: Stephen Hanson Date: Tue, 4 Apr 2023 22:48:41 -0500 Subject: [PATCH 04/24] Add missing tests --- src/__tests__/host-component-names.test.tsx | 2 +- src/__tests__/mapProps.test.tsx | 72 +++++++--- src/helpers/mapProps.ts | 11 +- src/queries/__tests__/a11yState.test.tsx | 96 ++++++++++++- src/queries/__tests__/a11yValue.test.tsx | 128 +++++++++++++---- src/queries/__tests__/displayValue.test.tsx | 120 ++++++++++++++-- src/queries/__tests__/hintText.test.tsx | 59 +++++++- src/queries/__tests__/labelText.test.tsx | 63 ++++++++- .../__tests__/placeholderText.test.tsx | 62 ++++++++- src/queries/__tests__/role-value.test.tsx | 109 ++++++++++++--- src/queries/__tests__/role.test.tsx | 130 +++++++++++++----- src/queries/__tests__/testId.test.tsx | 58 +++++++- src/queries/__tests__/text.test.tsx | 26 +++- 13 files changed, 798 insertions(+), 138 deletions(-) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 8d10c658d..4507e2278 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -87,7 +87,7 @@ describe('configureHostComponentNamesIfNeeded', () => { .toThrowErrorMatchingInlineSnapshot(` "Trying to detect host component names triggered the following error: - Unable to find an element with testID: text + \`render\` method has not been called 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." diff --git a/src/__tests__/mapProps.test.tsx b/src/__tests__/mapProps.test.tsx index c3d03ffd1..6f095aa82 100644 --- a/src/__tests__/mapProps.test.tsx +++ b/src/__tests__/mapProps.test.tsx @@ -8,36 +8,71 @@ const node: ReactTestRendererJSON = { }; describe('mapPropsForQueryError', () => { - test('preserves props that hide an element', () => { + test('preserves props that are helpful for debugging', () => { + const props = { + accessibilityElementsHidden: true, + accessibilityViewIsModal: true, + importantForAccessibility: 'yes', + testID: 'TEST_ID', + nativeID: 'NATIVE_ID', + accessibilityLabel: 'LABEL', + accessibilityLabelledBy: 'LABELLED_BY', + accessibilityRole: 'ROLE', + accessibilityHint: 'HINT', + placeholder: 'PLACEHOLDER', + value: 'VALUE', + defaultValue: 'DEFAULT_VALUE', + }; + + const result = mapPropsForQueryError(props, node); + + expect(result).toStrictEqual(props); + }); + + test('does not preserve less helpful props', () => { const result = mapPropsForQueryError( { - accessibilityElementsHidden: true, - importantForAccessibility: 'no-hide-descendants', - style: [{ flex: 1 }, { borderWidth: 2, display: 'none' }], + style: [{ flex: 1 }, { display: 'flex' }], + onPress: () => null, + key: 'foo', }, node ); + expect(result).toStrictEqual({}); + }); + + test('preserves "display: none" style but no other style', () => { + const result = mapPropsForQueryError( + { style: [{ flex: 1 }, { display: 'none', flex: 2 }] }, + node + ); + expect(result).toStrictEqual({ - accessibilityElementsHidden: true, - importantForAccessibility: 'no-hide-descendants', style: { display: 'none' }, }); }); test('removes undefined keys from accessibilityState', () => { const result = mapPropsForQueryError( - { - accessibilityState: { checked: undefined, selected: true }, - }, + { accessibilityState: { checked: undefined, selected: false } }, node ); expect(result).toStrictEqual({ - accessibilityState: { selected: true }, + accessibilityState: { selected: false }, }); }); + test('removes accessibilityState if all keys are undefined', () => { + const result = mapPropsForQueryError( + { accessibilityState: { checked: undefined, selected: undefined } }, + node + ); + + expect(result).toStrictEqual({}); + }); + test('does not fail if accessibilityState is a string, passes through', () => { const result = mapPropsForQueryError({ accessibilityState: 'foo' }, node); expect(result).toStrictEqual({ accessibilityState: 'foo' }); @@ -59,13 +94,18 @@ describe('mapPropsForQueryError', () => { expect(result).toStrictEqual({ accessibilityState: { 1: { 2: 3 } } }); }); - test('does not preserve props that do not hide', () => { + test('removes undefined keys from accessibilityValue', () => { const result = mapPropsForQueryError( - { - style: [{ flex: 1 }, { display: 'flex' }], - onPress: () => null, - key: 'foo', - }, + { accessibilityValue: { min: 1, max: undefined } }, + node + ); + + expect(result).toStrictEqual({ accessibilityValue: { min: 1 } }); + }); + + test('removes accessibilityValue if all keys are undefined', () => { + const result = mapPropsForQueryError( + { accessibilityValue: { min: undefined } }, node ); diff --git a/src/helpers/mapProps.ts b/src/helpers/mapProps.ts index 5d19dad1c..4d26cbd28 100644 --- a/src/helpers/mapProps.ts +++ b/src/helpers/mapProps.ts @@ -22,7 +22,7 @@ function isObject( return typeof thing === 'object' && !Array.isArray(thing) && thing !== null; } -function removeEmptyKeys(prop: unknown) { +function removeUndefinedKeys(prop: unknown) { if (isObject(prop)) { const object = Object.keys(prop).reduce((acc, propName) => { return { @@ -31,7 +31,10 @@ function removeEmptyKeys(prop: unknown) { }; }, {}); - if (!Object.values(object).find((val) => val !== undefined)) { + const allValuesUndefined = + Object.values(object).findIndex((val) => val !== undefined) === -1; + + if (allValuesUndefined) { return undefined; } @@ -45,8 +48,8 @@ function removeEmptyKeys(prop: unknown) { * Preserve props that are helpful in diagnosing test failures, while stripping rest */ export const mapPropsForQueryError: MapPropsFunction = (props) => { - const accessibilityState = removeEmptyKeys(props.accessibilityState); - const accessibilityValue = removeEmptyKeys(props.accessibilityValue); + const accessibilityState = removeUndefinedKeys(props.accessibilityState); + const accessibilityValue = removeUndefinedKeys(props.accessibilityValue); const styles = StyleSheet.flatten(props.style as any) ?? {}; diff --git a/src/queries/__tests__/a11yState.test.tsx b/src/queries/__tests__/a11yState.test.tsx index 41ed4b7c0..54d114483 100644 --- a/src/queries/__tests__/a11yState.test.tsx +++ b/src/queries/__tests__/a11yState.test.tsx @@ -254,9 +254,26 @@ test('byA11yState queries support hidden option', () => { ).toBeFalsy(); expect(() => getByA11yState({ expanded: false }, { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with expanded state: false"` - ); + ).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with expanded state: false + +  +  + Hidden from accessibility +  + " + `); }); test('*ByA11yState deprecation warnings', () => { @@ -352,3 +369,76 @@ test('*ByAccessibilityState deprecation warnings', () => { Use findAllByRole(role, { disabled, selected, checked, busy, expanded }) query or expect(...).toHaveAccessibilityState(...) matcher from "@testing-library/jest-native" package instead." `); }); + +test('error message renders the React DOM, preserving only helpful props', async () => { + const { + getByA11yState, + getAllByA11yState, + findByA11yState, + findAllByA11yState, + } = render( + null}> + Some text + + ); + + expect(() => getByA11yState({ checked: true })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with checked state: true + +  + Some text + " + `); + + expect(() => getAllByA11yState({ checked: true })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with checked state: true + +  + Some text + " + `); + + await expect(() => findByA11yState({ checked: true })).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with checked state: true + +  + Some text + " + `); + + await expect(() => findAllByA11yState({ checked: true })).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with checked state: true + +  + Some text + " + `); +}); diff --git a/src/queries/__tests__/a11yValue.test.tsx b/src/queries/__tests__/a11yValue.test.tsx index a3f6fb74e..154a75030 100644 --- a/src/queries/__tests__/a11yValue.test.tsx +++ b/src/queries/__tests__/a11yValue.test.tsx @@ -107,35 +107,53 @@ test('byA11yValue queries support hidden option', () => { expect( queryByA11yValue({ max: 10 }, { includeHiddenElements: false }) ).toBeFalsy(); - expect(() => - getByA11yValue({ max: 10 }, { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with max value: 10"` - ); + expect(() => getByA11yValue({ max: 10 }, { includeHiddenElements: false })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with max value: 10 + +  + Hidden from accessibility + " + `); }); test('byA11yValue error messages', () => { const { getByA11yValue } = render(); - expect(() => - getByA11yValue({ min: 10, max: 10 }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with min value: 10, max value: 10"` - ); - expect(() => - getByA11yValue({ max: 20, now: 5 }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with max value: 20, now value: 5"` - ); - expect(() => - getByA11yValue({ min: 1, max: 2, now: 3 }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with min value: 1, max value: 2, now value: 3"` - ); - expect(() => - getByA11yValue({ min: 1, max: 2, now: 3, text: /foo/i }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with min value: 1, max value: 2, now value: 3, text value: /foo/i"` - ); + expect(() => getByA11yValue({ min: 10, max: 10 })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with min value: 10, max value: 10 + + " + `); + expect(() => getByA11yValue({ max: 20, now: 5 })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with max value: 20, now value: 5 + + " + `); + expect(() => getByA11yValue({ min: 1, max: 2, now: 3 })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with min value: 1, max value: 2, now value: 3 + + " + `); + expect(() => getByA11yValue({ min: 1, max: 2, now: 3, text: /foo/i })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with min value: 1, max value: 2, now value: 3, text value: /foo/i + + " + `); }); test('*ByA11yValue deprecation warnings', () => { @@ -231,3 +249,63 @@ test('*ByAccessibilityValue deprecation warnings', () => { Use expect(...).toHaveAccessibilityValue(...) matcher from "@testing-library/jest-native" package or findAllByRole(role, { value: ... }) query instead." `); }); + +test('error message renders the React DOM, preserving only helpful props', async () => { + const { + getByA11yValue, + getAllByA11yValue, + findByA11yValue, + findAllByA11yValue, + } = render(); + + expect(() => getByA11yValue({ min: 1 })).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with min value: 1 + + " + `); + + expect(() => getAllByA11yValue({ min: 1 })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with min value: 1 + + " + `); + + await expect(() => findByA11yValue({ min: 1 })).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with min value: 1 + + " + `); + + await expect(() => findAllByA11yValue({ min: 1 })).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with min value: 1 + + " + `); +}); diff --git a/src/queries/__tests__/displayValue.test.tsx b/src/queries/__tests__/displayValue.test.tsx index c1c7f3713..34a394f64 100644 --- a/src/queries/__tests__/displayValue.test.tsx +++ b/src/queries/__tests__/displayValue.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { View, TextInput } from 'react-native'; +import { TextInput, View } from 'react-native'; import { render } from '../..'; @@ -50,16 +50,57 @@ test('getByDisplayValue, queryByDisplayValue', () => { test('getByDisplayValue, queryByDisplayValue get element by default value only when value is undefined', () => { const { getByDisplayValue, queryByDisplayValue } = render(); - expect(() => - getByDisplayValue(DEFAULT_INPUT_CHEF) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with displayValue: What did you inspect?"` - ); + 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(() => getByDisplayValue('hello')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with displayValue: hello + +  +  +  +  +  + " + `); expect(queryByDisplayValue('hello')).toBeNull(); expect(getByDisplayValue(DEFAULT_INPUT_CUSTOMER)).toBeTruthy(); @@ -119,11 +160,19 @@ test('byDisplayValue queries support hidden option', () => { expect( queryByDisplayValue('hidden', { includeHiddenElements: false }) ).toBeFalsy(); - expect(() => - getByDisplayValue('hidden', { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with displayValue: hidden"` - ); + expect(() => getByDisplayValue('hidden', { includeHiddenElements: false })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with displayValue: hidden + + " + `); }); test('byDisplayValue should return host component', () => { @@ -131,3 +180,46 @@ test('byDisplayValue should return host component', () => { expect(getByDisplayValue('value').type).toBe('TextInput'); }); + +test('error message renders the React DOM, preserving only helpful props', async () => { + const { + getByDisplayValue, + getAllByDisplayValue, + findByDisplayValue, + findAllByDisplayValue, + } = render(); + + expect(() => getByDisplayValue('2')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with displayValue: 2 + + " + `); + + expect(() => getAllByDisplayValue('2')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with displayValue: 2 + + " + `); + + await expect(() => findByDisplayValue('2')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with displayValue: 2 + + " + `); + + await expect(() => findAllByDisplayValue('2')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with displayValue: 2 + + " + `); +}); diff --git a/src/queries/__tests__/hintText.test.tsx b/src/queries/__tests__/hintText.test.tsx index d97427e3c..fabbee595 100644 --- a/src/queries/__tests__/hintText.test.tsx +++ b/src/queries/__tests__/hintText.test.tsx @@ -120,9 +120,58 @@ test('byHintText queries support hidden option', () => { expect( queryByHintText('hidden', { includeHiddenElements: false }) ).toBeFalsy(); - expect(() => - getByHintText('hidden', { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with accessibilityHint: hidden"` - ); + expect(() => getByHintText('hidden', { includeHiddenElements: false })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityHint: hidden + +  + Hidden from accessiblity + " + `); +}); + +test('error message renders the React DOM, preserving only helpful props', async () => { + const { getByHintText, getAllByHintText, findByHintText, findAllByHintText } = + render(); + + expect(() => getByHintText('FOO')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityHint: FOO + + " + `); + + expect(() => getAllByHintText('FOO')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityHint: FOO + + " + `); + + await expect(() => findByHintText('FOO')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityHint: FOO + + " + `); + + await expect(() => findAllByHintText('FOO')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityHint: FOO + + " + `); }); diff --git a/src/queries/__tests__/labelText.test.tsx b/src/queries/__tests__/labelText.test.tsx index da6de4ec4..f646823bf 100644 --- a/src/queries/__tests__/labelText.test.tsx +++ b/src/queries/__tests__/labelText.test.tsx @@ -159,11 +159,21 @@ test('byLabelText queries support hidden option', () => { expect( queryByLabelText('hidden', { includeHiddenElements: false }) ).toBeFalsy(); - expect(() => - getByLabelText('hidden', { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with accessibilityLabel: hidden"` - ); + expect(() => getByLabelText('hidden', { includeHiddenElements: false })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityLabel: hidden + +  + Hidden from accessibility + " + `); }); test('getByLabelText supports accessibilityLabelledBy', async () => { @@ -191,3 +201,46 @@ test('getByLabelText supports nested accessibilityLabelledBy', async () => { expect(getByLabelText('Label for input')).toBe(getByTestId('textInput')); expect(getByLabelText(/input/)).toBe(getByTestId('textInput')); }); + +test('error message renders the React DOM, preserving only helpful props', async () => { + const { + getByLabelText, + getAllByLabelText, + findByLabelText, + findAllByLabelText, + } = render(); + + expect(() => getByLabelText('FOO')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityLabel: FOO + + " + `); + + expect(() => getAllByLabelText('FOO')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityLabel: FOO + + " + `); + + await expect(() => findByLabelText('FOO')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityLabel: FOO + + " + `); + + await expect(() => findAllByLabelText('FOO')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with accessibilityLabel: FOO + + " + `); +}); diff --git a/src/queries/__tests__/placeholderText.test.tsx b/src/queries/__tests__/placeholderText.test.tsx index 276a301bb..6335ed81a 100644 --- a/src/queries/__tests__/placeholderText.test.tsx +++ b/src/queries/__tests__/placeholderText.test.tsx @@ -72,11 +72,19 @@ test('byPlaceholderText queries support hidden option', () => { expect( queryByPlaceholderText('hidden', { includeHiddenElements: false }) ).toBeFalsy(); - expect(() => - getByPlaceholderText('hidden', { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with placeholder: hidden"` - ); + expect(() => getByPlaceholderText('hidden', { includeHiddenElements: false })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with placeholder: hidden + + " + `); }); test('byPlaceHolderText should return host component', () => { @@ -86,3 +94,47 @@ test('byPlaceHolderText should return host component', () => { expect(getByPlaceholderText('placeholder').type).toBe('TextInput'); }); + +test('error message renders the React DOM, preserving only helpful props', async () => { + const { + getByPlaceholderText, + getAllByPlaceholderText, + findByPlaceholderText, + findAllByPlaceholderText, + } = render(); + + expect(() => getByPlaceholderText('FOO')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with placeholder: FOO + + " + `); + + expect(() => getAllByPlaceholderText('FOO')) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with placeholder: FOO + + " + `); + + await expect(() => findByPlaceholderText('FOO')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with placeholder: FOO + + " + `); + + await expect(() => findAllByPlaceholderText('FOO')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with placeholder: FOO + + " + `); +}); diff --git a/src/queries/__tests__/role-value.test.tsx b/src/queries/__tests__/role-value.test.tsx index 80c2c3ea8..e06326d2a 100644 --- a/src/queries/__tests__/role-value.test.tsx +++ b/src/queries/__tests__/role-value.test.tsx @@ -79,25 +79,98 @@ describe('accessibility value', () => { getByRole('adjustable', { disabled: true, value: { min: 10 } }) ).toBeTruthy(); - expect(() => - getByRole('adjustable', { name: 'Hello', value: { min: 5 } }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "adjustable", name: "Hello", min value: 5"` - ); - expect(() => - getByRole('adjustable', { name: 'World', value: { min: 10 } }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "adjustable", name: "World", min value: 10"` - ); - expect(() => - getByRole('adjustable', { name: 'Hello', value: { min: 5 } }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "adjustable", name: "Hello", min value: 5"` - ); + expect(() => getByRole('adjustable', { name: 'Hello', value: { min: 5 } })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "adjustable", name: "Hello", min value: 5 + +  + Hello + " + `); + expect(() => getByRole('adjustable', { name: 'World', value: { min: 10 } })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "adjustable", name: "World", min value: 10 + +  + Hello + " + `); + expect(() => getByRole('adjustable', { name: 'Hello', value: { min: 5 } })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "adjustable", name: "Hello", min value: 5 + +  + Hello + " + `); expect(() => getByRole('adjustable', { selected: true, value: { min: 10 } }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "adjustable", selected state: true, min value: 10"` - ); + ).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "adjustable", selected state: true, min value: 10 + +  + Hello + " + `); }); }); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index f953061d6..a580d5155 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -652,29 +652,33 @@ describe('error messages', () => { test('gives a descriptive error message when querying with a role', () => { const { getByRole } = render(); - expect(() => getByRole('button')).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button""` - ); + expect(() => getByRole('button')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "button" + + " + `); }); test('gives a descriptive error message when querying with a role and a name', () => { const { getByRole } = render(); - expect(() => - getByRole('button', { name: 'Save' }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button", name: "Save""` - ); + expect(() => getByRole('button', { name: 'Save' })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "button", name: "Save" + + " + `); }); test('gives a descriptive error message when querying with a role, a name and accessibility state', () => { const { getByRole } = render(); - expect(() => - getByRole('button', { name: 'Save', disabled: true }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button", name: "Save", disabled state: true"` - ); + expect(() => getByRole('button', { name: 'Save', disabled: true })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "button", name: "Save", disabled state: true + + " + `); }); test('gives a descriptive error message when querying with a role, a name and several accessibility state', () => { @@ -682,37 +686,43 @@ describe('error messages', () => { expect(() => getByRole('button', { name: 'Save', disabled: true, selected: true }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button", name: "Save", disabled state: true, selected state: true"` - ); + ).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "button", name: "Save", disabled state: true, selected state: true + + " + `); }); test('gives a descriptive error message when querying with a role and an accessibility state', () => { const { getByRole } = render(); - expect(() => - getByRole('button', { disabled: true }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button", disabled state: true"` - ); + expect(() => getByRole('button', { disabled: true })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "button", disabled state: true + + " + `); }); test('gives a descriptive error message when querying with a role and an accessibility value', () => { const { getByRole } = render(); - expect(() => - getByRole('adjustable', { value: { min: 1 } }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "adjustable", min value: 1"` - ); + expect(() => getByRole('adjustable', { value: { min: 1 } })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "adjustable", min value: 1 + + " + `); expect(() => getByRole('adjustable', { value: { min: 1, max: 2, now: 1, text: /hello/ }, }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "adjustable", min value: 1, max value: 2, now value: 1, text value: /hello/"` - ); + ).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "adjustable", min value: 1, max value: 2, now value: 1, text value: /hello/ + + " + `); }); }); @@ -727,11 +737,23 @@ test('byRole queries support hidden option', () => { expect(queryByRole('button')).toBeFalsy(); expect(queryByRole('button', { includeHiddenElements: false })).toBeFalsy(); - expect(() => - getByRole('button', { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button""` - ); + expect(() => getByRole('button', { includeHiddenElements: false })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "button" + +  +  + Hidden from accessibility +  + " + `); }); describe('matches only accessible elements', () => { @@ -762,3 +784,43 @@ describe('matches only accessible elements', () => { expect(queryByRole('menu', { name: 'Action' })).toBeFalsy(); }); }); + +test('error message renders the React DOM, preserving only helpful props', async () => { + const { getByRole, getAllByRole, findByRole, findAllByRole } = render( + + ); + + expect(() => getByRole('link')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "link" + + " + `); + + expect(() => getAllByRole('link')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "link" + + " + `); + + await expect(() => findByRole('link')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "link" + + " + `); + + await expect(() => findAllByRole('link')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with role: "link" + + " + `); +}); diff --git a/src/queries/__tests__/testId.test.tsx b/src/queries/__tests__/testId.test.tsx index ae615ea86..0ad380f8d 100644 --- a/src/queries/__tests__/testId.test.tsx +++ b/src/queries/__tests__/testId.test.tsx @@ -144,9 +144,59 @@ test('byTestId queries support hidden option', () => { expect(queryByTestId('hidden')).toBeFalsy(); expect(queryByTestId('hidden', { includeHiddenElements: false })).toBeFalsy(); - expect(() => - getByTestId('hidden', { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with testID: hidden"` + expect(() => getByTestId('hidden', { includeHiddenElements: false })) + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with testID: hidden + +  + Hidden from accessibility + " + `); +}); + +test('error message renders the React DOM, preserving only helpful props', async () => { + const { getByTestId, getAllByTestId, findByTestId, findAllByTestId } = render( + ); + + expect(() => getByTestId('FOO')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with testID: FOO + + " + `); + + expect(() => getAllByTestId('FOO')).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with testID: FOO + + " + `); + + await expect(() => findByTestId('FOO')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with testID: FOO + + " + `); + + await expect(() => findAllByTestId('FOO')).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with testID: FOO + + " + `); }); diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index 390529860..aa3330a45 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -501,8 +501,8 @@ test('byText support hidden option', () => { ); }); -describe('byText error message', () => { - test('renders the React DOM with regular props stripped', async () => { +describe('error messages', () => { + test('renders the React DOM with less helpful props stripped', async () => { const { getByText } = render( null}>Some text); expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` @@ -546,6 +546,7 @@ describe('byText error message', () => { accessibilityLabel="LABEL" accessibilityLabelledBy="LABELLED_BY" accessibilityRole="summary" + accessibilityViewIsModal={true} importantForAccessibility="yes" nativeID="NATIVE_ID" testID="TEST_ID" @@ -562,11 +563,19 @@ describe('byText error message', () => { `); }); - test('works with findBy', async () => { - const { findByText } = render( + test('also filters props with getAllBy, findBy, findAllBy', async () => { + const { getAllByText, findByText, findAllByText } = render( ); + expect(() => getAllByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + " + `); + await expect(() => findByText(/foo/)).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ @@ -575,6 +584,15 @@ describe('byText error message', () => { accessibilityViewIsModal={true} />" `); + + await expect(() => findAllByText(/foo/)).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + " + `); }); test('does not strip display: none from "style" prop, but does strip other styles', () => { From 4f0389f4b0f0a7388587c59de8cfc9b6277ba28c Mon Sep 17 00:00:00 2001 From: Stephen Hanson Date: Tue, 4 Apr 2023 23:35:38 -0500 Subject: [PATCH 05/24] Optimize findBy, findAllBy to only render DOM on timeout --- src/queries/__tests__/text.test.tsx | 48 ++++++++++++++- src/queries/makeQueries.ts | 92 +++++++++++++++++++++++++---- src/waitFor.ts | 5 +- 3 files changed, 133 insertions(+), 12 deletions(-) diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index aa3330a45..7a7e37b2c 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -7,7 +7,7 @@ import { Button, TextInput, } from 'react-native'; -import { render, getDefaultNormalizer, within } from '../..'; +import { render, getDefaultNormalizer, within, screen } from '../..'; test('byText matches simple text', () => { const { getByText } = render(Hello World); @@ -595,6 +595,52 @@ describe('error messages', () => { `); }); + test('only renders the DOM on last failure with findBy', async () => { + const { findByText } = render( + + ); + + jest.spyOn(screen, 'toJSON'); + + await expect(() => findByText(/foo/)).rejects.toThrow(); + + expect(screen.toJSON).toHaveBeenCalledTimes(1); + }); + + test('can still modify findBy error in custom onTimeout', async () => { + const { findByText } = render( + + ); + + jest.spyOn(screen, 'toJSON'); + const onTimeout = jest.fn(); + + await expect(() => + findByText(/foo/, undefined, { + onTimeout, + }) + ).rejects.toThrow(); + + expect(screen.toJSON).toHaveBeenCalledTimes(1); + expect(onTimeout).toHaveBeenCalledTimes(1); + }); + + test('only renders the DOM on last failure with findAllBy', async () => { + const { findAllByText } = render( + + ); + + jest.spyOn(screen, 'toJSON'); + const onTimeout = jest.fn(); + + await expect(() => + findAllByText(/foo/, undefined, { onTimeout }) + ).rejects.toThrow(); + + expect(screen.toJSON).toHaveBeenCalledTimes(1); + expect(onTimeout).toHaveBeenCalledTimes(1); + }); + test('does not strip display: none from "style" prop, but does strip other styles', () => { const { getByText } = render( diff --git a/src/queries/makeQueries.ts b/src/queries/makeQueries.ts index 792ad2136..b69e8ff5c 100644 --- a/src/queries/makeQueries.ts +++ b/src/queries/makeQueries.ts @@ -101,12 +101,21 @@ export function makeQueries( getMultipleError: (predicate: Predicate, options?: Options) => string ): UnboundQueries { function getAllByQuery(instance: ReactTestInstance) { + return getAllByQueryInternal(instance, { printDOM: true }); + } + + function getAllByQueryInternal( + instance: ReactTestInstance, + { printDOM = true } = {} + ) { return function getAllFn(predicate: Predicate, options?: Options) { const results = queryAllByQuery(instance)(predicate, options); if (results.length === 0) { throw new ErrorWithStack( - `${getMissingError(predicate, options)}${getFormattedDOM()}`, + `${getMissingError(predicate, options)}${ + printDOM ? getFormattedDOM() : '' // avoid formatting DOM if timeout not reached in findAllBy + }`, getAllFn ); } @@ -135,6 +144,13 @@ export function makeQueries( } function getByQuery(instance: ReactTestInstance) { + return getByQueryInternal(instance, { printDOM: true }); + } + + function getByQueryInternal( + instance: ReactTestInstance, + { printDOM = true } = {} + ) { return function getFn(predicate: Predicate, options?: Options) { const results = queryAllByQuery(instance)(predicate, options); @@ -144,7 +160,9 @@ export function makeQueries( if (results.length === 0) { throw new ErrorWithStack( - `${getMissingError(predicate, options)}${getFormattedDOM()}`, + `${getMissingError(predicate, options)}${ + printDOM ? getFormattedDOM() : '' // avoid formatting DOM if timeout not reached in findBy + }`, getFn ); } @@ -161,10 +179,37 @@ export function makeQueries( ) { const deprecatedWaitForOptions = extractDeprecatedWaitForOptions(queryOptions); - return waitFor(() => getAllByQuery(instance)(predicate, queryOptions), { - ...deprecatedWaitForOptions, - ...waitForOptions, - }); + + // append formatted DOM to final error + const onTimeout = (e: unknown) => { + const error = e as Error; + if (error?.message) { + error.message = `${error.message}${getFormattedDOM()}`; + } + + if (waitForOptions.onTimeout) { + return waitForOptions.onTimeout(error); + } + + if (deprecatedWaitForOptions?.onTimeout) { + return deprecatedWaitForOptions.onTimeout(error); + } + + return error; + }; + + return waitFor( + () => + getAllByQueryInternal(instance, { printDOM: false })( + predicate, + queryOptions + ), + { + ...deprecatedWaitForOptions, + ...waitForOptions, + onTimeout, + } + ); }; } @@ -176,10 +221,37 @@ export function makeQueries( ) { const deprecatedWaitForOptions = extractDeprecatedWaitForOptions(queryOptions); - return waitFor(() => getByQuery(instance)(predicate, queryOptions), { - ...deprecatedWaitForOptions, - ...waitForOptions, - }); + + // append formatted DOM to final error + const onTimeout = (e: unknown) => { + const error = e as Error; + if (error?.message) { + error.message = `${error.message}${getFormattedDOM()}`; + } + + if (waitForOptions.onTimeout) { + return waitForOptions.onTimeout(error); + } + + if (deprecatedWaitForOptions?.onTimeout) { + return deprecatedWaitForOptions.onTimeout(error); + } + + return error; + }; + + return waitFor( + () => + getByQueryInternal(instance, { printDOM: false })( + predicate, + queryOptions + ), + { + ...deprecatedWaitForOptions, + ...waitForOptions, + onTimeout, + } + ); }; } diff --git a/src/waitFor.ts b/src/waitFor.ts index 58feb224a..bb902b20b 100644 --- a/src/waitFor.ts +++ b/src/waitFor.ts @@ -177,7 +177,10 @@ function waitForInternal( } } if (typeof onTimeout === 'function') { - onTimeout(error); + const result = onTimeout(error); + if (result) { + error = result; + } } onDone({ type: 'error', error }); } From d7cf14b8152a6d0e507c2cca790ab7b873fe3dda Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 5 Apr 2023 18:45:59 +0200 Subject: [PATCH 06/24] refactor: make queries clean up --- src/queries/makeQueries.ts | 64 ++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/queries/makeQueries.ts b/src/queries/makeQueries.ts index b69e8ff5c..8823dca47 100644 --- a/src/queries/makeQueries.ts +++ b/src/queries/makeQueries.ts @@ -86,13 +86,17 @@ Example: return waitForOptions; } -/** - * @returns formatted DOM with two newlines preceding - */ -function getFormattedDOM() { - return ` +function formatErrorMessage(message: string, printElementTree: boolean) { + if (!printElementTree) { + return message; + } + + const json = screen.toJSON(); + if (!json) { + return message; + } -${format(screen.toJSON() || [], { mapProps: mapPropsForQueryError })}`; + return `${message}\n\n${format(json, { mapProps: mapPropsForQueryError })}`; } export function makeQueries( @@ -100,22 +104,19 @@ export function makeQueries( getMissingError: (predicate: Predicate, options?: Options) => string, getMultipleError: (predicate: Predicate, options?: Options) => string ): UnboundQueries { - function getAllByQuery(instance: ReactTestInstance) { - return getAllByQueryInternal(instance, { printDOM: true }); - } - - function getAllByQueryInternal( + function getAllByQuery( instance: ReactTestInstance, - { printDOM = true } = {} + { printElementTree = true } = {} ) { return function getAllFn(predicate: Predicate, options?: Options) { const results = queryAllByQuery(instance)(predicate, options); if (results.length === 0) { throw new ErrorWithStack( - `${getMissingError(predicate, options)}${ - printDOM ? getFormattedDOM() : '' // avoid formatting DOM if timeout not reached in findAllBy - }`, + formatErrorMessage( + getMissingError(predicate, options), + printElementTree + ), getAllFn ); } @@ -124,13 +125,19 @@ export function makeQueries( }; } - function queryByQuery(instance: ReactTestInstance) { + function queryByQuery( + instance: ReactTestInstance, + { printElementTree = true } = {} + ) { return function singleQueryFn(predicate: Predicate, options?: Options) { const results = queryAllByQuery(instance)(predicate, options); if (results.length > 1) { throw new ErrorWithStack( - getMultipleError(predicate, options), + formatErrorMessage( + getMultipleError(predicate, options), + printElementTree + ), singleQueryFn ); } @@ -143,13 +150,9 @@ export function makeQueries( }; } - function getByQuery(instance: ReactTestInstance) { - return getByQueryInternal(instance, { printDOM: true }); - } - - function getByQueryInternal( + function getByQuery( instance: ReactTestInstance, - { printDOM = true } = {} + { printElementTree = true } = {} ) { return function getFn(predicate: Predicate, options?: Options) { const results = queryAllByQuery(instance)(predicate, options); @@ -160,9 +163,10 @@ export function makeQueries( if (results.length === 0) { throw new ErrorWithStack( - `${getMissingError(predicate, options)}${ - printDOM ? getFormattedDOM() : '' // avoid formatting DOM if timeout not reached in findBy - }`, + formatErrorMessage( + getMissingError(predicate, options), + printElementTree + ), getFn ); } @@ -184,7 +188,7 @@ export function makeQueries( const onTimeout = (e: unknown) => { const error = e as Error; if (error?.message) { - error.message = `${error.message}${getFormattedDOM()}`; + error.message = formatErrorMessage(error.message, true); } if (waitForOptions.onTimeout) { @@ -200,7 +204,7 @@ export function makeQueries( return waitFor( () => - getAllByQueryInternal(instance, { printDOM: false })( + getAllByQuery(instance, { printElementTree: false })( predicate, queryOptions ), @@ -226,7 +230,7 @@ export function makeQueries( const onTimeout = (e: unknown) => { const error = e as Error; if (error?.message) { - error.message = `${error.message}${getFormattedDOM()}`; + error.message = formatErrorMessage(error.message, true); } if (waitForOptions.onTimeout) { @@ -242,7 +246,7 @@ export function makeQueries( return waitFor( () => - getByQueryInternal(instance, { printDOM: false })( + getByQuery(instance, { printElementTree: false })( predicate, queryOptions ), From f45b2421e9c95981cb818079fd614355406080b4 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 5 Apr 2023 18:54:57 +0200 Subject: [PATCH 07/24] refactor: remove color control codes from error --- src/helpers/format.ts | 3 +- src/queries/__tests__/a11yState.test.tsx | 116 ++++++++-------- src/queries/__tests__/a11yValue.test.tsx | 56 ++++---- src/queries/__tests__/displayValue.test.tsx | 130 +++++++++--------- src/queries/__tests__/hintText.test.tsx | 54 ++++---- src/queries/__tests__/labelText.test.tsx | 38 ++--- .../__tests__/placeholderText.test.tsx | 34 ++--- src/queries/__tests__/role-value.test.tsx | 72 +++++----- src/queries/__tests__/role.test.tsx | 56 ++++---- src/queries/__tests__/testId.test.tsx | 38 ++--- src/queries/__tests__/text.test.tsx | 104 +++++++------- src/queries/makeQueries.ts | 5 +- 12 files changed, 355 insertions(+), 351 deletions(-) diff --git a/src/helpers/format.ts b/src/helpers/format.ts index a24730968..12d085f00 100644 --- a/src/helpers/format.ts +++ b/src/helpers/format.ts @@ -8,6 +8,7 @@ export type MapPropsFunction = ( export type FormatOptions = { mapProps?: MapPropsFunction; + highlight?: boolean; }; const format = ( @@ -16,7 +17,7 @@ const format = ( ) => prettyFormat(input, { plugins: [getCustomPlugin(options.mapProps), plugins.ReactElement], - highlight: true, + highlight: options.highlight ?? true, }); const getCustomPlugin = (mapProps?: MapPropsFunction): NewPlugin => { diff --git a/src/queries/__tests__/a11yState.test.tsx b/src/queries/__tests__/a11yState.test.tsx index 54d114483..efec274ce 100644 --- a/src/queries/__tests__/a11yState.test.tsx +++ b/src/queries/__tests__/a11yState.test.tsx @@ -257,22 +257,22 @@ test('byA11yState queries support hidden option', () => { ).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with expanded state: false -  -  - Hidden from accessibility -  - " + } + > + + Hidden from accessibility + + " `); }); @@ -384,61 +384,61 @@ test('error message renders the React DOM, preserving only helpful props', async expect(() => getByA11yState({ checked: true })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with checked state: true - -  - Some text - " - `); + "Unable to find an element with checked state: true + + + Some text + " + `); expect(() => getAllByA11yState({ checked: true })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with checked state: true - -  - Some text - " - `); + "Unable to find an element with checked state: true + + + Some text + " + `); await expect(() => findByA11yState({ checked: true })).rejects .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with checked state: true - -  - Some text - " - `); + "Unable to find an element with checked state: true + + + Some text + " + `); await expect(() => findAllByA11yState({ checked: true })).rejects .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with checked state: true - -  - Some text - " - `); + "Unable to find an element with checked state: true + + + Some text + " + `); }); diff --git a/src/queries/__tests__/a11yValue.test.tsx b/src/queries/__tests__/a11yValue.test.tsx index 154a75030..2336cb28e 100644 --- a/src/queries/__tests__/a11yValue.test.tsx +++ b/src/queries/__tests__/a11yValue.test.tsx @@ -111,20 +111,20 @@ test('byA11yValue queries support hidden option', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with max value: 10 -  - Hidden from accessibility - " + } + > + Hidden from accessibility + " `); }); @@ -134,25 +134,25 @@ test('byA11yValue error messages', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with min value: 10, max value: 10 - " + " `); expect(() => getByA11yValue({ max: 20, now: 5 })) .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with max value: 20, now value: 5 - " + " `); expect(() => getByA11yValue({ min: 1, max: 2, now: 3 })) .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with min value: 1, max value: 2, now value: 3 - " + " `); expect(() => getByA11yValue({ min: 1, max: 2, now: 3, text: /foo/i })) .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with min value: 1, max value: 2, now value: 3, text value: /foo/i - " + " `); }); @@ -261,51 +261,51 @@ test('error message renders the React DOM, preserving only helpful props', async expect(() => getByA11yValue({ min: 1 })).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with min value: 1 - " + } + />" `); expect(() => getAllByA11yValue({ min: 1 })) .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with min value: 1 - " + } + />" `); await expect(() => findByA11yValue({ min: 1 })).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with min value: 1 - " + } + />" `); await expect(() => findAllByA11yValue({ min: 1 })).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with min value: 1 - " + } + />" `); }); diff --git a/src/queries/__tests__/displayValue.test.tsx b/src/queries/__tests__/displayValue.test.tsx index 34a394f64..d75eca6f3 100644 --- a/src/queries/__tests__/displayValue.test.tsx +++ b/src/queries/__tests__/displayValue.test.tsx @@ -54,52 +54,52 @@ test('getByDisplayValue, queryByDisplayValue get element by default value only w .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(); @@ -164,14 +164,14 @@ test('byDisplayValue queries support hidden option', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with displayValue: hidden - " + } + value="hidden" + />" `); }); @@ -190,36 +190,36 @@ test('error message renders the React DOM, preserving only helpful props', async } = render(); expect(() => getByDisplayValue('2')).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with displayValue: 2 + "Unable to find an element with displayValue: 2 - " - `); + " + `); expect(() => getAllByDisplayValue('2')).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with displayValue: 2 + "Unable to find an element with displayValue: 2 - " - `); + " + `); await expect(() => findByDisplayValue('2')).rejects .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with displayValue: 2 + "Unable to find an element with displayValue: 2 - " - `); + " + `); await expect(() => findAllByDisplayValue('2')).rejects .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with displayValue: 2 + "Unable to find an element with displayValue: 2 - " - `); + " + `); }); diff --git a/src/queries/__tests__/hintText.test.tsx b/src/queries/__tests__/hintText.test.tsx index fabbee595..70d7a535b 100644 --- a/src/queries/__tests__/hintText.test.tsx +++ b/src/queries/__tests__/hintText.test.tsx @@ -124,16 +124,16 @@ test('byHintText queries support hidden option', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with accessibilityHint: hidden -  - Hidden from accessiblity - " + } + > + Hidden from accessiblity + " `); }); @@ -142,36 +142,36 @@ test('error message renders the React DOM, preserving only helpful props', async render(); expect(() => getByHintText('FOO')).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with accessibilityHint: FOO + "Unable to find an element with accessibilityHint: FOO - " - `); + " + `); expect(() => getAllByHintText('FOO')).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with accessibilityHint: FOO + "Unable to find an element with accessibilityHint: FOO - " - `); + " + `); await expect(() => findByHintText('FOO')).rejects .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with accessibilityHint: FOO + "Unable to find an element with accessibilityHint: FOO - " - `); + " + `); await expect(() => findAllByHintText('FOO')).rejects .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with accessibilityHint: FOO + "Unable to find an element with accessibilityHint: FOO - " - `); + " + `); }); diff --git a/src/queries/__tests__/labelText.test.tsx b/src/queries/__tests__/labelText.test.tsx index f646823bf..822af7dd8 100644 --- a/src/queries/__tests__/labelText.test.tsx +++ b/src/queries/__tests__/labelText.test.tsx @@ -163,16 +163,16 @@ test('byLabelText queries support hidden option', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with accessibilityLabel: hidden -  - Hidden from accessibility - " + } + > + Hidden from accessibility + " `); }); @@ -213,34 +213,34 @@ test('error message renders the React DOM, preserving only helpful props', async expect(() => getByLabelText('FOO')).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with accessibilityLabel: FOO - " + " `); expect(() => getAllByLabelText('FOO')).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with accessibilityLabel: FOO - " + " `); await expect(() => findByLabelText('FOO')).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with accessibilityLabel: FOO - " + " `); await expect(() => findAllByLabelText('FOO')).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with accessibilityLabel: FOO - " + " `); }); diff --git a/src/queries/__tests__/placeholderText.test.tsx b/src/queries/__tests__/placeholderText.test.tsx index 6335ed81a..ca95c898a 100644 --- a/src/queries/__tests__/placeholderText.test.tsx +++ b/src/queries/__tests__/placeholderText.test.tsx @@ -76,14 +76,14 @@ test('byPlaceholderText queries support hidden option', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with placeholder: hidden - " + } + />" `); }); @@ -106,35 +106,35 @@ test('error message renders the React DOM, preserving only helpful props', async expect(() => getByPlaceholderText('FOO')).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with placeholder: FOO - " + " `); expect(() => getAllByPlaceholderText('FOO')) .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with placeholder: FOO - " + " `); await expect(() => findByPlaceholderText('FOO')).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with placeholder: FOO - " + " `); await expect(() => findAllByPlaceholderText('FOO')).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with placeholder: FOO - " + " `); }); diff --git a/src/queries/__tests__/role-value.test.tsx b/src/queries/__tests__/role-value.test.tsx index e06326d2a..d73705d59 100644 --- a/src/queries/__tests__/role-value.test.tsx +++ b/src/queries/__tests__/role-value.test.tsx @@ -83,94 +83,94 @@ describe('accessibility value', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "adjustable", name: "Hello", min value: 5 -  - Hello - " + } + > + Hello + " `); expect(() => getByRole('adjustable', { name: 'World', value: { min: 10 } })) .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "adjustable", name: "World", min value: 10 -  - Hello - " + } + > + Hello + " `); expect(() => getByRole('adjustable', { name: 'Hello', value: { min: 5 } })) .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "adjustable", name: "Hello", min value: 5 -  - Hello - " + } + > + Hello + " `); expect(() => getByRole('adjustable', { selected: true, value: { min: 10 } }) ).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "adjustable", selected state: true, min value: 10 -  - Hello - " + } + > + Hello + " `); }); }); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index a580d5155..305911bf8 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -655,7 +655,7 @@ describe('error messages', () => { expect(() => getByRole('button')).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "button" - " + " `); }); @@ -666,7 +666,7 @@ describe('error messages', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "button", name: "Save" - " + " `); }); @@ -677,7 +677,7 @@ describe('error messages', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "button", name: "Save", disabled state: true - " + " `); }); @@ -689,7 +689,7 @@ describe('error messages', () => { ).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "button", name: "Save", disabled state: true, selected state: true - " + " `); }); @@ -700,7 +700,7 @@ describe('error messages', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "button", disabled state: true - " + " `); }); @@ -711,7 +711,7 @@ describe('error messages', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "adjustable", min value: 1 - " + " `); expect(() => @@ -721,7 +721,7 @@ describe('error messages', () => { ).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "adjustable", min value: 1, max value: 2, now value: 1, text value: /hello/ - " + " `); }); }); @@ -741,18 +741,18 @@ test('byRole queries support hidden option', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "button" -  -  - Hidden from accessibility -  - " + } + > + + Hidden from accessibility + + " `); }); @@ -793,34 +793,34 @@ test('error message renders the React DOM, preserving only helpful props', async expect(() => getByRole('link')).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "link" - " + " `); expect(() => getAllByRole('link')).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "link" - " + " `); await expect(() => findByRole('link')).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "link" - " + " `); await expect(() => findAllByRole('link')).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with role: "link" - " + " `); }); diff --git a/src/queries/__tests__/testId.test.tsx b/src/queries/__tests__/testId.test.tsx index 0ad380f8d..2ccf4549d 100644 --- a/src/queries/__tests__/testId.test.tsx +++ b/src/queries/__tests__/testId.test.tsx @@ -148,16 +148,16 @@ test('byTestId queries support hidden option', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with testID: hidden -  - Hidden from accessibility - " + } + testID="hidden" + > + Hidden from accessibility + " `); }); @@ -169,34 +169,34 @@ test('error message renders the React DOM, preserving only helpful props', async expect(() => getByTestId('FOO')).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with testID: FOO - " + " `); expect(() => getAllByTestId('FOO')).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with testID: FOO - " + " `); await expect(() => findByTestId('FOO')).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with testID: FOO - " + " `); await expect(() => findAllByTestId('FOO')).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with testID: FOO - " + " `); }); diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index 7a7e37b2c..dd7fd1cb3 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -508,9 +508,9 @@ describe('error messages', () => { expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ -  - Some text - " + + Some text + " `); }); @@ -540,26 +540,26 @@ describe('error messages', () => { expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ -  -  -  - Some Text -  - " + + + + Some Text + + " `); }); @@ -571,27 +571,27 @@ describe('error messages', () => { expect(() => getAllByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ - " + " `); await expect(() => findByText(/foo/)).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ - " + " `); await expect(() => findAllByText(/foo/)).rejects .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ - " + " `); }); @@ -658,17 +658,17 @@ describe('error messages', () => { expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ -  -  +  - Some text -  - " + } + > + Some text + + " `); }); @@ -682,16 +682,16 @@ describe('error messages', () => { expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ -  -  - " + } + > + + " `); }); @@ -705,15 +705,15 @@ describe('error messages', () => { expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ -  -  - " + } + > + + " `); }); }); diff --git a/src/queries/makeQueries.ts b/src/queries/makeQueries.ts index 8823dca47..9b138d5ca 100644 --- a/src/queries/makeQueries.ts +++ b/src/queries/makeQueries.ts @@ -96,7 +96,10 @@ function formatErrorMessage(message: string, printElementTree: boolean) { return message; } - return `${message}\n\n${format(json, { mapProps: mapPropsForQueryError })}`; + return `${message}\n\n${format(json, { + highlight: false, + mapProps: mapPropsForQueryError, + })}`; } export function makeQueries( From 0a0bb3308c8b873e166555071d1fb9480f07dede Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 5 Apr 2023 21:44:27 +0200 Subject: [PATCH 08/24] refactor: disable format element coloring just for our tests --- jest-setup.ts | 3 +++ src/helpers/format.ts | 7 +++++-- src/queries/makeQueries.ts | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/jest-setup.ts b/jest-setup.ts index 5d2c4d5b2..948c468d1 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -5,3 +5,6 @@ jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); beforeEach(() => { resetToDefaults(); }); + +// Disable colors in our local tests in order to generate clear snapshots +process.env.COLORS = 'false'; diff --git a/src/helpers/format.ts b/src/helpers/format.ts index 12d085f00..726aead22 100644 --- a/src/helpers/format.ts +++ b/src/helpers/format.ts @@ -8,7 +8,6 @@ export type MapPropsFunction = ( export type FormatOptions = { mapProps?: MapPropsFunction; - highlight?: boolean; }; const format = ( @@ -17,7 +16,7 @@ const format = ( ) => prettyFormat(input, { plugins: [getCustomPlugin(options.mapProps), plugins.ReactElement], - highlight: options.highlight ?? true, + highlight: shouldHighlight(), }); const getCustomPlugin = (mapProps?: MapPropsFunction): NewPlugin => { @@ -40,4 +39,8 @@ const getCustomPlugin = (mapProps?: MapPropsFunction): NewPlugin => { }; }; +function shouldHighlight() { + return process?.env?.COLORS !== 'false'; +} + export default format; diff --git a/src/queries/makeQueries.ts b/src/queries/makeQueries.ts index 9b138d5ca..780dda4b0 100644 --- a/src/queries/makeQueries.ts +++ b/src/queries/makeQueries.ts @@ -97,7 +97,6 @@ function formatErrorMessage(message: string, printElementTree: boolean) { } return `${message}\n\n${format(json, { - highlight: false, mapProps: mapPropsForQueryError, })}`; } From e5d6080580a0e10fc6c3b1c1973155e94e5c1079 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 5 Apr 2023 21:56:37 +0200 Subject: [PATCH 09/24] fix: host elements error message --- src/__tests__/host-component-names.test.tsx | 8 ++++--- src/helpers/host-component-names.tsx | 24 +++++++++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 4507e2278..623640033 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -79,15 +79,17 @@ describe('configureHostComponentNamesIfNeeded', () => { }); test('throw an error when autodetection fails', () => { + const renderer = TestRenderer.create(); + mockCreate.mockReturnValue({ - root: { type: View, children: [], props: {} }, + root: renderer.root, }); expect(() => configureHostComponentNamesIfNeeded()) .toThrowErrorMatchingInlineSnapshot(` "Trying to detect host component names triggered the following error: - \`render\` method has not been called + 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." @@ -105,7 +107,7 @@ describe('configureHostComponentNamesIfNeeded', () => { .toThrowErrorMatchingInlineSnapshot(` "Trying to detect host component names triggered the following error: - getByTestId returned non-host component + 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." diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx index 475ba8518..213e88bf6 100644 --- a/src/helpers/host-component-names.tsx +++ b/src/helpers/host-component-names.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; +import { ReactTestInstance } from 'react-test-renderer'; import { Text, TextInput, View } from 'react-native'; import { configureInternal, getConfig, HostComponentNames } from '../config'; import { renderWithAct } from '../render-act'; -import { getQueriesForElement } from '../within'; 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.`; @@ -35,9 +35,9 @@ function detectHostComponentNames(): HostComponentNames { ); - const { getByTestId } = getQueriesForElement(renderer.root); - const textHostName = getByTestId('text').type; - const textInputHostName = getByTestId('textInput').type; + + const textHostName = getByTestId(renderer.root, 'text').type; + const textInputHostName = getByTestId(renderer.root, 'textInput').type; // This code path should not happen as getByTestId always returns host elements. if ( @@ -62,3 +62,19 @@ function detectHostComponentNames(): HostComponentNames { ); } } + +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}`); + } + + if (nodes.length > 1) { + throw new Error(`Found multiple elements with testID: ${testID}`); + } + + return nodes[0]; +} From 1b95f4d74998421f445accf432e7b916fad6fbdd Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 5 Apr 2023 22:03:54 +0200 Subject: [PATCH 10/24] refactor: cleanup host component names --- src/__tests__/host-component-names.test.tsx | 27 +++------------------ src/helpers/host-component-names.tsx | 15 ++---------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 623640033..1df43f267 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -6,14 +6,13 @@ import { getHostComponentNames, configureHostComponentNamesIfNeeded, } 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( - within, - 'getQueriesForElement' -) as jest.Mock; + +beforeEach(() => { + mockCreate.mockReset(); +}); describe('getHostComponentNames', () => { test('returns host component names from internal config', () => { @@ -95,22 +94,4 @@ describe('configureHostComponentNamesIfNeeded', () => { Please check if you are using compatible versions of React Native and React Native Testing Library." `); }); - - test('throw an error when autodetection fails due to getByTestId returning non-host component', () => { - mockGetQueriesForElements.mockReturnValue({ - getByTestId: () => { - return { type: View }; - }, - }); - - 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." - `); - }); }); diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx index 213e88bf6..f4e025824 100644 --- a/src/helpers/host-component-names.tsx +++ b/src/helpers/host-component-names.tsx @@ -36,20 +36,9 @@ function detectHostComponentNames(): HostComponentNames { ); - const textHostName = getByTestId(renderer.root, 'text').type; - const textInputHostName = getByTestId(renderer.root, 'textInput').type; - - // This code path should not happen as getByTestId always returns host elements. - if ( - typeof textHostName !== 'string' || - typeof textInputHostName !== 'string' - ) { - throw new Error('getByTestId returned non-host component'); - } - return { - text: textHostName, - textInput: textInputHostName, + text: getByTestId(renderer.root, 'text').type as string, + textInput: getByTestId(renderer.root, 'textInput').type as string, }; } catch (error) { const errorMessage = From 701c39eac6b61b5fc63c7e2d433815241e262dd4 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 5 Apr 2023 22:07:15 +0200 Subject: [PATCH 11/24] chore: reverse unnecessary reorder of imports --- src/queries/text.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queries/text.ts b/src/queries/text.ts index b687e2f3b..425f0153c 100644 --- a/src/queries/text.ts +++ b/src/queries/text.ts @@ -4,6 +4,7 @@ import { findAll } from '../helpers/findAll'; import { getHostComponentNames } from '../helpers/host-component-names'; import { matchTextContent } from '../helpers/matchers/matchTextContent'; import { TextMatch, TextMatchOptions } from '../matches'; +import { makeQueries } from './makeQueries'; import type { FindAllByQuery, FindByQuery, @@ -12,7 +13,6 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; -import { makeQueries } from './makeQueries'; import type { CommonQueryOptions } from './options'; type ByTextOptions = CommonQueryOptions & TextMatchOptions; From 6be5df795976bd31aa0ef1cb0f6db8dedd1537e4 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 5 Apr 2023 22:27:20 +0200 Subject: [PATCH 12/24] refactor: tweaks --- src/helpers/mapProps.ts | 61 ++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/src/helpers/mapProps.ts b/src/helpers/mapProps.ts index 4d26cbd28..2930af17d 100644 --- a/src/helpers/mapProps.ts +++ b/src/helpers/mapProps.ts @@ -2,46 +2,43 @@ import { StyleSheet } from 'react-native'; import { MapPropsFunction } from './format'; const propsToDisplay = [ + 'testID', + 'nativeID', 'accessibilityElementsHidden', 'accessibilityViewIsModal', 'importantForAccessibility', - 'testID', - 'nativeID', + 'accessibilityRole', 'accessibilityLabel', 'accessibilityLabelledBy', - 'accessibilityRole', 'accessibilityHint', 'placeholder', 'value', 'defaultValue', + 'title', ]; -function isObject( - thing: unknown -): thing is Record { - return typeof thing === 'object' && !Array.isArray(thing) && thing !== null; +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); } function removeUndefinedKeys(prop: unknown) { - if (isObject(prop)) { - const object = Object.keys(prop).reduce((acc, propName) => { - return { - ...acc, - ...(prop[propName] === undefined ? {} : { [propName]: prop[propName] }), - }; - }, {}); - - const allValuesUndefined = - Object.values(object).findIndex((val) => val !== undefined) === -1; + if (!isObject(prop)) { + return prop; + } - if (allValuesUndefined) { - return undefined; + const result: Record = {}; + Object.keys(prop).forEach((propName) => { + if (prop[propName] !== undefined) { + result[propName] = prop[propName]; } + }); - return object; + // If object does not have any props we will ignore it. + if (Object.keys(result).length === 0) { + return undefined; } - return prop; + return result; } /** @@ -51,24 +48,20 @@ export const mapPropsForQueryError: MapPropsFunction = (props) => { const accessibilityState = removeUndefinedKeys(props.accessibilityState); const accessibilityValue = removeUndefinedKeys(props.accessibilityValue); - const styles = StyleSheet.flatten(props.style as any) ?? {}; + const styles = StyleSheet.flatten(props.style) as any; // perform custom prop mappings - const mappedProps: Record = { - ...(styles.display === 'none' ? { style: { display: 'none' } } : undefined), - ...(accessibilityState === undefined ? {} : { accessibilityState }), - ...(accessibilityValue === undefined ? {} : { accessibilityValue }), + const result: Record = { + ...(styles?.display === 'none' ? { style: { display: 'none' } } : {}), + ...(accessibilityState !== undefined ? { accessibilityState } : {}), + ...(accessibilityValue !== undefined ? { accessibilityValue } : {}), }; - // add props from propsToDisplay without mapping - return propsToDisplay.reduce((acc, propName) => { + propsToDisplay.forEach((propName) => { if (propName in props) { - return { - ...acc, - [propName]: props[propName], - }; + result[propName] = props[propName]; } + }); - return acc; - }, mappedProps); + return result; }; From 5c7bb5549e2974bbcc43ac6f2aaa6630cda9f40c Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 5 Apr 2023 22:33:27 +0200 Subject: [PATCH 13/24] refactor: tweaks --- .../__tests__/format-default.tsx} | 24 ++++----- .../{mapProps.ts => format-default.ts} | 50 +++++++++---------- src/queries/makeQueries.ts | 4 +- 3 files changed, 39 insertions(+), 39 deletions(-) rename src/{__tests__/mapProps.test.tsx => helpers/__tests__/format-default.tsx} (80%) rename src/helpers/{mapProps.ts => format-default.ts} (96%) diff --git a/src/__tests__/mapProps.test.tsx b/src/helpers/__tests__/format-default.tsx similarity index 80% rename from src/__tests__/mapProps.test.tsx rename to src/helpers/__tests__/format-default.tsx index 6f095aa82..6cd49c5fc 100644 --- a/src/__tests__/mapProps.test.tsx +++ b/src/helpers/__tests__/format-default.tsx @@ -1,5 +1,5 @@ import { ReactTestRendererJSON } from 'react-test-renderer'; -import { mapPropsForQueryError } from '../helpers/mapProps'; +import { defaultMapProps } from '../format-default'; const node: ReactTestRendererJSON = { type: 'View', @@ -24,13 +24,13 @@ describe('mapPropsForQueryError', () => { defaultValue: 'DEFAULT_VALUE', }; - const result = mapPropsForQueryError(props, node); + const result = defaultMapProps(props, node); expect(result).toStrictEqual(props); }); test('does not preserve less helpful props', () => { - const result = mapPropsForQueryError( + const result = defaultMapProps( { style: [{ flex: 1 }, { display: 'flex' }], onPress: () => null, @@ -43,7 +43,7 @@ describe('mapPropsForQueryError', () => { }); test('preserves "display: none" style but no other style', () => { - const result = mapPropsForQueryError( + const result = defaultMapProps( { style: [{ flex: 1 }, { display: 'none', flex: 2 }] }, node ); @@ -54,7 +54,7 @@ describe('mapPropsForQueryError', () => { }); test('removes undefined keys from accessibilityState', () => { - const result = mapPropsForQueryError( + const result = defaultMapProps( { accessibilityState: { checked: undefined, selected: false } }, node ); @@ -65,7 +65,7 @@ describe('mapPropsForQueryError', () => { }); test('removes accessibilityState if all keys are undefined', () => { - const result = mapPropsForQueryError( + const result = defaultMapProps( { accessibilityState: { checked: undefined, selected: undefined } }, node ); @@ -74,28 +74,28 @@ describe('mapPropsForQueryError', () => { }); test('does not fail if accessibilityState is a string, passes through', () => { - const result = mapPropsForQueryError({ accessibilityState: 'foo' }, node); + const result = defaultMapProps({ accessibilityState: 'foo' }, node); expect(result).toStrictEqual({ accessibilityState: 'foo' }); }); test('does not fail if accessibilityState is an array, passes through', () => { - const result = mapPropsForQueryError({ accessibilityState: [1] }, node); + const result = defaultMapProps({ accessibilityState: [1] }, node); expect(result).toStrictEqual({ accessibilityState: [1] }); }); test('does not fail if accessibilityState is null, passes through', () => { - const result = mapPropsForQueryError({ accessibilityState: null }, node); + const result = defaultMapProps({ accessibilityState: null }, node); expect(result).toStrictEqual({ accessibilityState: null }); }); test('does not fail if accessibilityState is nested object, passes through', () => { const accessibilityState = { 1: { 2: 3 }, 2: undefined }; - const result = mapPropsForQueryError({ accessibilityState }, node); + const result = defaultMapProps({ accessibilityState }, node); expect(result).toStrictEqual({ accessibilityState: { 1: { 2: 3 } } }); }); test('removes undefined keys from accessibilityValue', () => { - const result = mapPropsForQueryError( + const result = defaultMapProps( { accessibilityValue: { min: 1, max: undefined } }, node ); @@ -104,7 +104,7 @@ describe('mapPropsForQueryError', () => { }); test('removes accessibilityValue if all keys are undefined', () => { - const result = mapPropsForQueryError( + const result = defaultMapProps( { accessibilityValue: { min: undefined } }, node ); diff --git a/src/helpers/mapProps.ts b/src/helpers/format-default.ts similarity index 96% rename from src/helpers/mapProps.ts rename to src/helpers/format-default.ts index 2930af17d..d4e482e6a 100644 --- a/src/helpers/mapProps.ts +++ b/src/helpers/format-default.ts @@ -17,34 +17,10 @@ const propsToDisplay = [ 'title', ]; -function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -function removeUndefinedKeys(prop: unknown) { - if (!isObject(prop)) { - return prop; - } - - const result: Record = {}; - Object.keys(prop).forEach((propName) => { - if (prop[propName] !== undefined) { - result[propName] = prop[propName]; - } - }); - - // If object does not have any props we will ignore it. - if (Object.keys(result).length === 0) { - return undefined; - } - - return result; -} - /** * Preserve props that are helpful in diagnosing test failures, while stripping rest */ -export const mapPropsForQueryError: MapPropsFunction = (props) => { +export const defaultMapProps: MapPropsFunction = (props) => { const accessibilityState = removeUndefinedKeys(props.accessibilityState); const accessibilityValue = removeUndefinedKeys(props.accessibilityValue); @@ -65,3 +41,27 @@ export const mapPropsForQueryError: MapPropsFunction = (props) => { return result; }; + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function removeUndefinedKeys(prop: unknown) { + if (!isObject(prop)) { + return prop; + } + + const result: Record = {}; + Object.keys(prop).forEach((propName) => { + if (prop[propName] !== undefined) { + result[propName] = prop[propName]; + } + }); + + // If object does not have any props we will ignore it. + if (Object.keys(result).length === 0) { + return undefined; + } + + return result; +} diff --git a/src/queries/makeQueries.ts b/src/queries/makeQueries.ts index 780dda4b0..3a51225b7 100644 --- a/src/queries/makeQueries.ts +++ b/src/queries/makeQueries.ts @@ -4,7 +4,7 @@ import waitFor from '../waitFor'; import type { WaitForOptions } from '../waitFor'; import format from '../helpers/format'; import { screen } from '../screen'; -import { mapPropsForQueryError } from '../helpers/mapProps'; +import { defaultMapProps } from '../helpers/format-default'; export type GetByQuery = ( predicate: Predicate, @@ -97,7 +97,7 @@ function formatErrorMessage(message: string, printElementTree: boolean) { } return `${message}\n\n${format(json, { - mapProps: mapPropsForQueryError, + mapProps: defaultMapProps, })}`; } From 7904d3a9c1b55af2af1d9e4411b1e0dabfee8006 Mon Sep 17 00:00:00 2001 From: Stephen Hanson Date: Wed, 19 Apr 2023 21:57:03 -0500 Subject: [PATCH 14/24] fix: fix issue where findBy* doesn't print tree * Also updates onTimeout so now matches the behavior of React DOM where if a custom onTimeout is passed, the error that it receives does not have the element tree attached. Reference: https://github.com/testing-library/dom-testing-library/blob/1fc17bec5d28e5b58fcdd325d6d2caaff02dfb47/src/wait-for.js#L26 * Moves general element tree tests to new makeQueries.test.tsx instead of text.test.tsx --- src/queries/__tests__/makeQueries.test.tsx | 257 +++++++++++++++++++++ src/queries/__tests__/text.test.tsx | 208 ++--------------- src/queries/makeQueries.ts | 56 ++--- 3 files changed, 290 insertions(+), 231 deletions(-) create mode 100644 src/queries/__tests__/makeQueries.test.tsx diff --git a/src/queries/__tests__/makeQueries.test.tsx b/src/queries/__tests__/makeQueries.test.tsx new file mode 100644 index 000000000..b17840104 --- /dev/null +++ b/src/queries/__tests__/makeQueries.test.tsx @@ -0,0 +1,257 @@ +import * as React from 'react'; +import { Text, TextInput, View } from 'react-native'; +import { render, screen } from '../..'; + +describe('printing element tree', () => { + test('includes element tree on error with less-helpful props stripped', async () => { + const { getByText } = render( null}>Some text); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + + Some text + " + `); + }); + + test('prints helpful props but not others', async () => { + const { getByText } = render( + + + Some Text + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + + + + Some Text + + " + `); + }); + + test('prints tree and filters props with getAllBy, findBy, findAllBy', async () => { + const { getAllByText, findByText, findAllByText } = render( + + ); + + expect(() => getAllByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + " + `); + + await expect(findByText(/foo/)).rejects.toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + " + `); + + await expect(() => findAllByText(/foo/)).rejects + .toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + " + `); + }); + + // the stack is what actually gets printed to the console, so we need to + // ensure that the element tree is in the stack and not just in the message + test('when findBy fails, includes element tree in stack, not just message', async () => { + const { findByText } = render(); + + let error: Error = new Error(); + + try { + await findByText(/foo/); + } catch (e) { + error = e as Error; + } finally { + expect(error?.stack).toMatch(/ { + const { findAllByText } = render(); + + let error: Error = new Error(); + + try { + await findAllByText(/foo/); + } catch (e) { + error = e as Error; + } finally { + expect(error?.stack).toMatch(//); + } + }); + + test('only appends element tree on last failure with findBy', async () => { + const { findByText } = render( + + ); + + jest.spyOn(screen, 'toJSON'); + + await expect(() => findByText(/foo/)).rejects.toThrow(); + + expect(screen.toJSON).toHaveBeenCalledTimes(1); + }); + + test('custom onTimeout with findBy receives error without element tree', async () => { + expect.assertions(3); + const { findByText } = render(); + + const onTimeout = jest.fn((e: unknown) => { + const error = e as Error; + // does not include the element tree + expect(error.message).not.toMatch(/View/); + return error; + }); + + await expect(() => + findByText(/foo/, undefined, { onTimeout }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with text: /foo/"` + ); + + expect(onTimeout).toHaveBeenCalledTimes(1); + }); + + test('custom onTimeout with findAllBy receives error without element tree', async () => { + expect.assertions(3); + const { findAllByText } = render(); + + const onTimeout = jest.fn((e: unknown) => { + const error = e as Error; + // does not include the element tree + expect(error.message).not.toMatch(/View/); + return error; + }); + + await expect(() => + findAllByText(/foo/, undefined, { onTimeout }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with text: /foo/"` + ); + + expect(onTimeout).toHaveBeenCalledTimes(1); + }); + + test('does not strip display: none from "style" prop, but does strip other styles', () => { + const { getByText } = render( + + + Some text + + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + + + Some text + + " + `); + }); + + test('strips undefined values from accessibilityState', () => { + const { getByText } = render( + + + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + + + " + `); + }); + + test('strips undefined values from accessibilityValue', () => { + const { getByText } = render( + + + + ); + + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + "Unable to find an element with text: /foo/ + + + + " + `); + }); +}); diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index dd7fd1cb3..39647a466 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import { - View, - Text, - TouchableOpacity, - Image, Button, + Image, + Text, TextInput, + TouchableOpacity, + View, } from 'react-native'; -import { render, getDefaultNormalizer, within, screen } from '../..'; +import { getDefaultNormalizer, render, within } from '../..'; test('byText matches simple text', () => { const { getByText } = render(Hello World); @@ -501,74 +501,20 @@ test('byText support hidden option', () => { ); }); -describe('error messages', () => { - test('renders the React DOM with less helpful props stripped', async () => { - const { getByText } = render( null}>Some text); - - expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with text: /foo/ - - - Some text - " - `); - }); - - test('passes through helpful props', async () => { - const { getByText } = render( - - - Some Text - - ); +test('error message renders the element tree, preserving only helpful props', async () => { + const { getByText, getAllByText, findByText, findAllByText } = render( + + ); - expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ - - - Some Text - - " + />" `); - }); - test('also filters props with getAllBy, findBy, findAllBy', async () => { - const { getAllByText, findByText, findAllByText } = render( - - ); - - expect(() => getAllByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` + expect(() => getAllByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ { />" `); - await expect(() => findByText(/foo/)).rejects - .toThrowErrorMatchingInlineSnapshot(` + await expect(findByText(/foo/)).rejects.toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ { />" `); - await expect(() => findAllByText(/foo/)).rejects - .toThrowErrorMatchingInlineSnapshot(` + await expect(() => findAllByText(/foo/)).rejects + .toThrowErrorMatchingInlineSnapshot(` "Unable to find an element with text: /foo/ " `); - }); - - test('only renders the DOM on last failure with findBy', async () => { - const { findByText } = render( - - ); - - jest.spyOn(screen, 'toJSON'); - - await expect(() => findByText(/foo/)).rejects.toThrow(); - - expect(screen.toJSON).toHaveBeenCalledTimes(1); - }); - - test('can still modify findBy error in custom onTimeout', async () => { - const { findByText } = render( - - ); - - jest.spyOn(screen, 'toJSON'); - const onTimeout = jest.fn(); - - await expect(() => - findByText(/foo/, undefined, { - onTimeout, - }) - ).rejects.toThrow(); - - expect(screen.toJSON).toHaveBeenCalledTimes(1); - expect(onTimeout).toHaveBeenCalledTimes(1); - }); - - test('only renders the DOM on last failure with findAllBy', async () => { - const { findAllByText } = render( - - ); - - jest.spyOn(screen, 'toJSON'); - const onTimeout = jest.fn(); - - await expect(() => - findAllByText(/foo/, undefined, { onTimeout }) - ).rejects.toThrow(); - - expect(screen.toJSON).toHaveBeenCalledTimes(1); - expect(onTimeout).toHaveBeenCalledTimes(1); - }); - - test('does not strip display: none from "style" prop, but does strip other styles', () => { - const { getByText } = render( - - - Some text - - - ); - - expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with text: /foo/ - - - - Some text - - " - `); - }); - - test('strips undefined values from accessibilityState', () => { - const { getByText } = render( - - - - ); - - expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with text: /foo/ - - - - " - `); - }); - - test('strips undefined values from accessibilityValue', () => { - const { getByText } = render( - - - - ); - - expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with text: /foo/ - - - - " - `); - }); }); test('byText should return host component', () => { diff --git a/src/queries/makeQueries.ts b/src/queries/makeQueries.ts index 3a51225b7..d03509ece 100644 --- a/src/queries/makeQueries.ts +++ b/src/queries/makeQueries.ts @@ -101,6 +101,16 @@ function formatErrorMessage(message: string, printElementTree: boolean) { })}`; } +function appendElementTreeToError(error: Error) { + if (error?.message) { + const oldMessage = error.message; + error.message = formatErrorMessage(oldMessage, true); + error.stack = error.stack?.replace(oldMessage, error.message); + } + + return error; +} + export function makeQueries( queryAllByQuery: UnboundQuery>, getMissingError: (predicate: Predicate, options?: Options) => string, @@ -181,29 +191,14 @@ export function makeQueries( return function findAllFn( predicate: Predicate, queryOptions?: Options & WaitForOptions, - waitForOptions: WaitForOptions = {} + { + onTimeout = (e: unknown) => appendElementTreeToError(e as Error), + ...waitForOptions + }: WaitForOptions = {} ) { const deprecatedWaitForOptions = extractDeprecatedWaitForOptions(queryOptions); - // append formatted DOM to final error - const onTimeout = (e: unknown) => { - const error = e as Error; - if (error?.message) { - error.message = formatErrorMessage(error.message, true); - } - - if (waitForOptions.onTimeout) { - return waitForOptions.onTimeout(error); - } - - if (deprecatedWaitForOptions?.onTimeout) { - return deprecatedWaitForOptions.onTimeout(error); - } - - return error; - }; - return waitFor( () => getAllByQuery(instance, { printElementTree: false })( @@ -223,29 +218,14 @@ export function makeQueries( return function findFn( predicate: Predicate, queryOptions?: Options & WaitForOptions, - waitForOptions: WaitForOptions = {} + { + onTimeout = (e: unknown) => appendElementTreeToError(e as Error), + ...waitForOptions + }: WaitForOptions = {} ) { const deprecatedWaitForOptions = extractDeprecatedWaitForOptions(queryOptions); - // append formatted DOM to final error - const onTimeout = (e: unknown) => { - const error = e as Error; - if (error?.message) { - error.message = formatErrorMessage(error.message, true); - } - - if (waitForOptions.onTimeout) { - return waitForOptions.onTimeout(error); - } - - if (deprecatedWaitForOptions?.onTimeout) { - return deprecatedWaitForOptions.onTimeout(error); - } - - return error; - }; - return waitFor( () => getByQuery(instance, { printElementTree: false })( From 6d3d50c05fb76083f1a9de2ae4e0e8126c9f9ef7 Mon Sep 17 00:00:00 2001 From: Stephen Hanson Date: Wed, 19 Apr 2023 22:04:42 -0500 Subject: [PATCH 15/24] refactor: update wording 'DOM' -> 'element tree' --- src/queries/__tests__/a11yState.test.tsx | 2 +- src/queries/__tests__/a11yValue.test.tsx | 2 +- src/queries/__tests__/displayValue.test.tsx | 2 +- src/queries/__tests__/hintText.test.tsx | 2 +- src/queries/__tests__/labelText.test.tsx | 2 +- src/queries/__tests__/placeholderText.test.tsx | 2 +- src/queries/__tests__/role.test.tsx | 2 +- src/queries/__tests__/testId.test.tsx | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/queries/__tests__/a11yState.test.tsx b/src/queries/__tests__/a11yState.test.tsx index efec274ce..3af1e19f6 100644 --- a/src/queries/__tests__/a11yState.test.tsx +++ b/src/queries/__tests__/a11yState.test.tsx @@ -370,7 +370,7 @@ test('*ByAccessibilityState deprecation warnings', () => { `); }); -test('error message renders the React DOM, preserving only helpful props', async () => { +test('error message renders the element tree, preserving only helpful props', async () => { const { getByA11yState, getAllByA11yState, diff --git a/src/queries/__tests__/a11yValue.test.tsx b/src/queries/__tests__/a11yValue.test.tsx index 2336cb28e..5944a140f 100644 --- a/src/queries/__tests__/a11yValue.test.tsx +++ b/src/queries/__tests__/a11yValue.test.tsx @@ -250,7 +250,7 @@ test('*ByAccessibilityValue deprecation warnings', () => { `); }); -test('error message renders the React DOM, preserving only helpful props', async () => { +test('error message renders the element tree, preserving only helpful props', async () => { const { getByA11yValue, getAllByA11yValue, diff --git a/src/queries/__tests__/displayValue.test.tsx b/src/queries/__tests__/displayValue.test.tsx index d75eca6f3..4f84ba1d0 100644 --- a/src/queries/__tests__/displayValue.test.tsx +++ b/src/queries/__tests__/displayValue.test.tsx @@ -181,7 +181,7 @@ test('byDisplayValue should return host component', () => { expect(getByDisplayValue('value').type).toBe('TextInput'); }); -test('error message renders the React DOM, preserving only helpful props', async () => { +test('error message renders the element tree, preserving only helpful props', async () => { const { getByDisplayValue, getAllByDisplayValue, diff --git a/src/queries/__tests__/hintText.test.tsx b/src/queries/__tests__/hintText.test.tsx index 70d7a535b..9731c043f 100644 --- a/src/queries/__tests__/hintText.test.tsx +++ b/src/queries/__tests__/hintText.test.tsx @@ -137,7 +137,7 @@ test('byHintText queries support hidden option', () => { `); }); -test('error message renders the React DOM, preserving only helpful props', async () => { +test('error message renders the element tree, preserving only helpful props', async () => { const { getByHintText, getAllByHintText, findByHintText, findAllByHintText } = render(); diff --git a/src/queries/__tests__/labelText.test.tsx b/src/queries/__tests__/labelText.test.tsx index 822af7dd8..b8bb12ebf 100644 --- a/src/queries/__tests__/labelText.test.tsx +++ b/src/queries/__tests__/labelText.test.tsx @@ -202,7 +202,7 @@ test('getByLabelText supports nested accessibilityLabelledBy', async () => { expect(getByLabelText(/input/)).toBe(getByTestId('textInput')); }); -test('error message renders the React DOM, preserving only helpful props', async () => { +test('error message renders the element tree, preserving only helpful props', async () => { const { getByLabelText, getAllByLabelText, diff --git a/src/queries/__tests__/placeholderText.test.tsx b/src/queries/__tests__/placeholderText.test.tsx index ca95c898a..522dfbff2 100644 --- a/src/queries/__tests__/placeholderText.test.tsx +++ b/src/queries/__tests__/placeholderText.test.tsx @@ -95,7 +95,7 @@ test('byPlaceHolderText should return host component', () => { expect(getByPlaceholderText('placeholder').type).toBe('TextInput'); }); -test('error message renders the React DOM, preserving only helpful props', async () => { +test('error message renders the element tree, preserving only helpful props', async () => { const { getByPlaceholderText, getAllByPlaceholderText, diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index 305911bf8..ee39766a8 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -785,7 +785,7 @@ describe('matches only accessible elements', () => { }); }); -test('error message renders the React DOM, preserving only helpful props', async () => { +test('error message renders the element tree, preserving only helpful props', async () => { const { getByRole, getAllByRole, findByRole, findAllByRole } = render( ); diff --git a/src/queries/__tests__/testId.test.tsx b/src/queries/__tests__/testId.test.tsx index 2ccf4549d..de08c190a 100644 --- a/src/queries/__tests__/testId.test.tsx +++ b/src/queries/__tests__/testId.test.tsx @@ -161,7 +161,7 @@ test('byTestId queries support hidden option', () => { `); }); -test('error message renders the React DOM, preserving only helpful props', async () => { +test('error message renders the element tree, preserving only helpful props', async () => { const { getByTestId, getAllByTestId, findByTestId, findAllByTestId } = render( ); From 8f4258430ed609aec50266605cdc4e3b06c6630b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 26 Apr 2023 23:37:30 +0200 Subject: [PATCH 16/24] refactor: tweaks --- .../__snapshots__/render-debug.test.tsx.snap | 18 +++++------ src/helpers/format-default.ts | 31 +++++++++++-------- src/helpers/format.ts | 1 + src/helpers/host-component-names.tsx | 4 --- src/queries/__tests__/a11yState.test.tsx | 12 +++---- src/queries/__tests__/a11yValue.test.tsx | 12 +++---- src/queries/__tests__/displayValue.test.tsx | 2 +- src/queries/__tests__/hintText.test.tsx | 2 +- src/queries/__tests__/labelText.test.tsx | 2 +- src/queries/__tests__/makeQueries.test.tsx | 6 ++-- .../__tests__/placeholderText.test.tsx | 2 +- src/queries/__tests__/role-value.test.tsx | 16 +++++----- src/queries/__tests__/role.test.tsx | 2 +- src/queries/__tests__/testId.test.tsx | 2 +- 14 files changed, 57 insertions(+), 55 deletions(-) diff --git a/src/__tests__/__snapshots__/render-debug.test.tsx.snap b/src/__tests__/__snapshots__/render-debug.test.tsx.snap index 2b44623dd..e72298ccc 100644 --- a/src/__tests__/__snapshots__/render-debug.test.tsx.snap +++ b/src/__tests__/__snapshots__/render-debug.test.tsx.snap @@ -30,7 +30,7 @@ exports[`debug 1`] = ` /> { - const accessibilityState = removeUndefinedKeys(props.accessibilityState); - const accessibilityValue = removeUndefinedKeys(props.accessibilityValue); + const result: Record = {}; - const styles = StyleSheet.flatten(props.style) as any; + const styles = StyleSheet.flatten(props.style as ViewStyle); + if (styles?.display === 'none') { + result.style = { display: 'none' }; + } - // perform custom prop mappings - const result: Record = { - ...(styles?.display === 'none' ? { style: { display: 'none' } } : {}), - ...(accessibilityState !== undefined ? { accessibilityState } : {}), - ...(accessibilityValue !== undefined ? { accessibilityValue } : {}), - }; + const accessibilityState = removeUndefinedKeys(props.accessibilityState); + if (accessibilityState !== undefined) { + result.accessibilityState = accessibilityState; + } + + const accessibilityValue = removeUndefinedKeys(props.accessibilityValue); + if (accessibilityValue !== undefined) { + result.accessibilityValue = accessibilityValue; + } propsToDisplay.forEach((propName) => { if (propName in props) { @@ -52,9 +57,9 @@ function removeUndefinedKeys(prop: unknown) { } const result: Record = {}; - Object.keys(prop).forEach((propName) => { - if (prop[propName] !== undefined) { - result[propName] = prop[propName]; + Object.entries(prop).forEach(([key, value]) => { + if (value !== undefined) { + result[key] = value; } }); diff --git a/src/helpers/format.ts b/src/helpers/format.ts index 726aead22..3bffc5917 100644 --- a/src/helpers/format.ts +++ b/src/helpers/format.ts @@ -17,6 +17,7 @@ const format = ( prettyFormat(input, { plugins: [getCustomPlugin(options.mapProps), plugins.ReactElement], highlight: shouldHighlight(), + printBasicPrototype: false, }); const getCustomPlugin = (mapProps?: MapPropsFunction): NewPlugin => { diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx index f4e025824..ac91cc71a 100644 --- a/src/helpers/host-component-names.tsx +++ b/src/helpers/host-component-names.tsx @@ -61,9 +61,5 @@ function getByTestId(instance: ReactTestInstance, testID: string) { throw new Error(`Unable to find an element with testID: ${testID}`); } - if (nodes.length > 1) { - throw new Error(`Found multiple elements with testID: ${testID}`); - } - return nodes[0]; } diff --git a/src/queries/__tests__/a11yState.test.tsx b/src/queries/__tests__/a11yState.test.tsx index 3af1e19f6..7d5f858b8 100644 --- a/src/queries/__tests__/a11yState.test.tsx +++ b/src/queries/__tests__/a11yState.test.tsx @@ -259,12 +259,12 @@ test('byA11yState queries support hidden option', () => {