diff --git a/README.md b/README.md index 2b8383025..44a2d8d22 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ test('form submits two answers', () => { fireEvent.press(screen.getByText('Submit')); expect(mockFn).toBeCalledWith({ - '1': { q: 'q1', a: 'a1' }, - '2': { q: 'q2', a: 'a2' }, + 1: { q: 'q1', a: 'a1' }, + 2: { q: 'q2', a: 'a2' }, }); }); ``` @@ -173,4 +173,4 @@ Supported and used by [Rally Health](https://www.rallyhealth.com/careers-home). [callstack-badge]: https://callstack.com/images/callstack-badge.svg [callstack]: https://callstack.com/open-source/?utm_source=github.com&utm_medium=referral&utm_campaign=react-native-testing-library&utm_term=readme [codecov-badge]: https://codecov.io/gh/callstack/react-native-testing-library/branch/main/graph/badge.svg?token=tYVSWro1IP -[codecov]: https://codecov.io/gh/callstack/react-native-testing-library \ No newline at end of file +[codecov]: https://codecov.io/gh/callstack/react-native-testing-library diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 5e3707ed8..5879b8ee2 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -14,6 +14,7 @@ test('configure() overrides existing config values', () => { expect(getConfig()).toEqual({ asyncUtilTimeout: 5000, defaultDebugOptions: { message: 'debug message' }, + defaultHidden: true, }); }); diff --git a/src/config.ts b/src/config.ts index 65d617935..b8133c3b9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,17 +4,19 @@ export type Config = { /** Default timeout, in ms, for `waitFor` and `findBy*` queries. */ asyncUtilTimeout: number; + /** Default hidden value for all queries */ + defaultHidden: boolean; + /** Default options for `debug` helper. */ defaultDebugOptions?: Partial; }; const defaultConfig: Config = { asyncUtilTimeout: 1000, + defaultHidden: true, }; -let config = { - ...defaultConfig, -}; +let config = { ...defaultConfig }; export function configure(options: Partial) { config = { @@ -24,7 +26,7 @@ export function configure(options: Partial) { } export function resetToDefaults() { - config = defaultConfig; + config = { ...defaultConfig }; } export function getConfig() { diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts index 0cccb7b6b..9ab4df787 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -2,6 +2,10 @@ import { AccessibilityState, StyleSheet } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; import { getHostSiblings } from './component-tree'; +type IsInaccessibleOptions = { + cache?: WeakMap; +}; + export type AccessibilityStateKey = keyof AccessibilityState; export const accessibilityStateKeys: AccessibilityStateKey[] = [ @@ -12,14 +16,24 @@ export const accessibilityStateKeys: AccessibilityStateKey[] = [ 'expanded', ]; -export function isInaccessible(element: ReactTestInstance | null): boolean { +export function isInaccessible( + element: ReactTestInstance | null, + { cache }: IsInaccessibleOptions = {} +): boolean { if (element == null) { return true; } let current: ReactTestInstance | null = element; while (current) { - if (isSubtreeInaccessible(current)) { + let isCurrentSubtreeInaccessible = cache?.get(current); + + if (isCurrentSubtreeInaccessible === undefined) { + isCurrentSubtreeInaccessible = isSubtreeInaccessible(current); + cache?.set(current, isCurrentSubtreeInaccessible); + } + + if (isCurrentSubtreeInaccessible) { return true; } @@ -29,7 +43,9 @@ export function isInaccessible(element: ReactTestInstance | null): boolean { return false; } -function isSubtreeInaccessible(element: ReactTestInstance | null): boolean { +export function isSubtreeInaccessible( + element: ReactTestInstance | null +): boolean { if (element == null) { return true; } @@ -46,7 +62,7 @@ function isSubtreeInaccessible(element: ReactTestInstance | null): boolean { return true; } - // Note that `opacity: 0` is not threated as inassessible on iOS + // Note that `opacity: 0` is not treated as inaccessible on iOS const flatStyle = StyleSheet.flatten(element.props.style) ?? {}; if (flatStyle.display === 'none') return true; diff --git a/src/helpers/findAll.ts b/src/helpers/findAll.ts new file mode 100644 index 000000000..779b8f0b4 --- /dev/null +++ b/src/helpers/findAll.ts @@ -0,0 +1,23 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import { getConfig } from '../config'; +import { isInaccessible } from './accessiblity'; + +interface FindAllOptions { + hidden?: boolean; +} + +export function findAll( + root: ReactTestInstance, + predicate: (node: ReactTestInstance) => boolean, + options?: FindAllOptions +) { + const results = root.findAll(predicate); + + const hidden = options?.hidden ?? getConfig().defaultHidden; + if (hidden) { + return results; + } + + const cache = new WeakMap(); + return results.filter((element) => !isInaccessible(element, { cache })); +} diff --git a/src/queries/__tests__/a11yState.test.tsx b/src/queries/__tests__/a11yState.test.tsx index c97ad70cb..73dc1cc57 100644 --- a/src/queries/__tests__/a11yState.test.tsx +++ b/src/queries/__tests__/a11yState.test.tsx @@ -226,3 +226,22 @@ test('*ByA11yState on TouchableOpacity with "disabled" prop', () => { expect(view.getByA11yState({ disabled: true })).toBeTruthy(); expect(view.queryByA11yState({ disabled: false })).toBeFalsy(); }); + +test('byA11yState queries support hidden option', () => { + const { getByA11yState, queryByA11yState } = render( + + Hidden from accessibility + + ); + + expect(getByA11yState({ expanded: false })).toBeTruthy(); + expect(getByA11yState({ expanded: false }, { hidden: true })).toBeTruthy(); + + expect(queryByA11yState({ expanded: false }, { hidden: false })).toBeFalsy(); + expect(() => + getByA11yState({ expanded: false }, { hidden: false }) + ).toThrow(); +}); diff --git a/src/queries/__tests__/a11yValue.test.tsx b/src/queries/__tests__/a11yValue.test.tsx index ddb4201a4..c1b7c84b9 100644 --- a/src/queries/__tests__/a11yValue.test.tsx +++ b/src/queries/__tests__/a11yValue.test.tsx @@ -92,3 +92,17 @@ test('getAllByA11yValue, queryAllByA11yValue, findAllByA11yValue', async () => { ); await expect(findAllByA11yValue({ max: 60 })).resolves.toHaveLength(2); }); + +test('byA11yValue queries support hidden option', () => { + const { getByA11yValue, queryByA11yValue } = render( + + Hidden from accessibility + + ); + + expect(getByA11yValue({ max: 10 })).toBeTruthy(); + expect(getByA11yValue({ max: 10 }, { hidden: true })).toBeTruthy(); + + expect(queryByA11yValue({ max: 10 }, { hidden: false })).toBeFalsy(); + expect(() => getByA11yValue({ max: 10 }, { hidden: false })).toThrow(); +}); diff --git a/src/queries/__tests__/displayValue.test.tsx b/src/queries/__tests__/displayValue.test.tsx index 3eb150551..ff2567ea0 100644 --- a/src/queries/__tests__/displayValue.test.tsx +++ b/src/queries/__tests__/displayValue.test.tsx @@ -99,3 +99,15 @@ test('findBy queries work asynchronously', async () => { await expect(findByDisplayValue('Display Value')).resolves.toBeTruthy(); await expect(findAllByDisplayValue('Display Value')).resolves.toHaveLength(1); }, 20000); + +test('byDisplayValue queries support hidden option', () => { + const { getByDisplayValue, queryByDisplayValue } = render( + + ); + + expect(getByDisplayValue('hidden')).toBeTruthy(); + expect(getByDisplayValue('hidden', { hidden: true })).toBeTruthy(); + + expect(queryByDisplayValue('hidden', { hidden: false })).toBeFalsy(); + expect(() => getByDisplayValue('hidden', { hidden: false })).toThrow(); +}); diff --git a/src/queries/__tests__/hintText.test.tsx b/src/queries/__tests__/hintText.test.tsx index 9a7585920..02ad9c596 100644 --- a/src/queries/__tests__/hintText.test.tsx +++ b/src/queries/__tests__/hintText.test.tsx @@ -106,3 +106,17 @@ test('getByHintText, getByHintText and exact = true', () => { expect(queryByHintText('id', { exact: true })).toBeNull(); expect(getAllByHintText('test', { exact: true })).toHaveLength(1); }); + +test('byHintText queries support hidden option', () => { + const { getByHintText, queryByHintText } = render( + + Hidden from accessiblity + + ); + + expect(getByHintText('hidden')).toBeTruthy(); + expect(getByHintText('hidden', { hidden: true })).toBeTruthy(); + + expect(queryByHintText('hidden', { hidden: false })).toBeFalsy(); + expect(() => getByHintText('hidden', { hidden: false })).toThrow(); +}); diff --git a/src/queries/__tests__/labelText.test.tsx b/src/queries/__tests__/labelText.test.tsx index 68564fa02..d1ce5a51b 100644 --- a/src/queries/__tests__/labelText.test.tsx +++ b/src/queries/__tests__/labelText.test.tsx @@ -143,3 +143,17 @@ describe('findBy options deprecations', () => { ); }, 20000); }); + +test('byLabelText queries support hidden option', () => { + const { getByLabelText, queryByLabelText } = render( + + Hidden from accessibility + + ); + + expect(getByLabelText('hidden')).toBeTruthy(); + expect(getByLabelText('hidden', { hidden: true })).toBeTruthy(); + + expect(queryByLabelText('hidden', { hidden: false })).toBeFalsy(); + expect(() => getByLabelText('hidden', { hidden: false })).toThrow(); +}); diff --git a/src/queries/__tests__/placeholderText.test.tsx b/src/queries/__tests__/placeholderText.test.tsx index 165e61b45..973691eee 100644 --- a/src/queries/__tests__/placeholderText.test.tsx +++ b/src/queries/__tests__/placeholderText.test.tsx @@ -58,3 +58,15 @@ test('getAllByPlaceholderText, queryAllByPlaceholderText', () => { expect(queryAllByPlaceholderText(/fresh/i)).toEqual(inputs); expect(queryAllByPlaceholderText('no placeholder')).toHaveLength(0); }); + +test('byPlaceholderText queries support hidden option', () => { + const { getByPlaceholderText, queryByPlaceholderText } = render( + + ); + + expect(getByPlaceholderText('hidden')).toBeTruthy(); + expect(getByPlaceholderText('hidden', { hidden: true })).toBeTruthy(); + + expect(queryByPlaceholderText('hidden', { hidden: false })).toBeFalsy(); + expect(() => getByPlaceholderText('hidden', { hidden: false })).toThrow(); +}); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index ce7519186..25e5c9cc1 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -697,3 +697,17 @@ describe('error messages', () => { ); }); }); + +test('byRole queries support hidden option', () => { + const { getByRole, queryByRole } = render( + + Hidden from accessibility + + ); + + expect(getByRole('button')).toBeTruthy(); + expect(getByRole('button', { hidden: true })).toBeTruthy(); + + expect(queryByRole('button', { hidden: false })).toBeFalsy(); + expect(() => getByRole('button', { hidden: false })).toThrow(); +}); diff --git a/src/queries/__tests__/testId.test.tsx b/src/queries/__tests__/testId.test.tsx index 6d97ac4b0..f60ebe3b7 100644 --- a/src/queries/__tests__/testId.test.tsx +++ b/src/queries/__tests__/testId.test.tsx @@ -132,3 +132,17 @@ test('findByTestId and findAllByTestId work asynchronously', async () => { await expect(findByTestId('aTestId')).resolves.toBeTruthy(); await expect(findAllByTestId('aTestId')).resolves.toHaveLength(1); }, 20000); + +test('byTestId queries support hidden option', () => { + const { getByTestId, queryByTestId } = render( + + Hidden from accessibility + + ); + + expect(getByTestId('hidden')).toBeTruthy(); + expect(getByTestId('hidden', { hidden: true })).toBeTruthy(); + + expect(queryByTestId('hidden', { hidden: false })).toBeFalsy(); + expect(() => getByTestId('hidden', { hidden: false })).toThrow(); +}); diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index 6f5c435b6..9195e22bb 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -466,3 +466,15 @@ test('getByText searches for text within self host element', () => { const textNode = within(getByTestId('subject')); expect(textNode.getByText('Hello')).toBeTruthy(); }); + +test('byText support hidden option', () => { + const { getByText, queryByText } = render( + Hidden from accessibility + ); + + expect(getByText(/hidden/i)).toBeTruthy(); + expect(getByText(/hidden/i, { hidden: true })).toBeTruthy(); + + expect(queryByText(/hidden/i, { hidden: false })).toBeFalsy(); + expect(() => getByText(/hidden/i, { hidden: false })).toThrow(); +}); diff --git a/src/queries/a11yState.ts b/src/queries/a11yState.ts index 7bbb415f7..84ca4bcb9 100644 --- a/src/queries/a11yState.ts +++ b/src/queries/a11yState.ts @@ -1,6 +1,7 @@ import type { ReactTestInstance } from 'react-test-renderer'; -import type { AccessibilityState } from 'react-native'; +import { AccessibilityState } from 'react-native'; import { accessibilityStateKeys } from '../helpers/accessiblity'; +import { findAll } from '../helpers/findAll'; import { matchAccessibilityState } from '../helpers/matchers/accessibilityState'; import { makeQueries } from './makeQueries'; import type { @@ -11,14 +12,20 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; +import { CommonQueryOptions } from './options'; const queryAllByA11yState = ( instance: ReactTestInstance -): ((matcher: AccessibilityState) => Array) => - function queryAllByA11yStateFn(matcher) { - return instance.findAll( +): (( + matcher: AccessibilityState, + queryOptions?: CommonQueryOptions +) => Array) => + function queryAllByA11yStateFn(matcher, queryOptions) { + return findAll( + instance, (node) => - typeof node.type === 'string' && matchAccessibilityState(node, matcher) + typeof node.type === 'string' && matchAccessibilityState(node, matcher), + queryOptions ); }; @@ -47,19 +54,31 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( ); export type ByA11yStateQueries = { - getByA11yState: GetByQuery; - getAllByA11yState: GetAllByQuery; - queryByA11yState: QueryByQuery; - queryAllByA11yState: QueryAllByQuery; - findByA11yState: FindByQuery; - findAllByA11yState: FindAllByQuery; + getByA11yState: GetByQuery; + getAllByA11yState: GetAllByQuery; + queryByA11yState: QueryByQuery; + queryAllByA11yState: QueryAllByQuery; + findByA11yState: FindByQuery; + findAllByA11yState: FindAllByQuery; - getByAccessibilityState: GetByQuery; - getAllByAccessibilityState: GetAllByQuery; - queryByAccessibilityState: QueryByQuery; - queryAllByAccessibilityState: QueryAllByQuery; - findByAccessibilityState: FindByQuery; - findAllByAccessibilityState: FindAllByQuery; + getByAccessibilityState: GetByQuery; + getAllByAccessibilityState: GetAllByQuery< + AccessibilityState, + CommonQueryOptions + >; + queryByAccessibilityState: QueryByQuery< + AccessibilityState, + CommonQueryOptions + >; + queryAllByAccessibilityState: QueryAllByQuery< + AccessibilityState, + CommonQueryOptions + >; + findByAccessibilityState: FindByQuery; + findAllByAccessibilityState: FindAllByQuery< + AccessibilityState, + CommonQueryOptions + >; }; export const bindByA11yStateQueries = ( diff --git a/src/queries/a11yValue.ts b/src/queries/a11yValue.ts index 8e68957e3..0c1f680b6 100644 --- a/src/queries/a11yValue.ts +++ b/src/queries/a11yValue.ts @@ -1,4 +1,5 @@ import type { ReactTestInstance } from 'react-test-renderer'; +import { findAll } from '../helpers/findAll'; import { matchObjectProp } from '../helpers/matchers/matchObjectProp'; import { makeQueries } from './makeQueries'; import type { @@ -9,6 +10,7 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; +import { CommonQueryOptions } from './options'; type A11yValue = { min?: number; @@ -19,12 +21,17 @@ type A11yValue = { const queryAllByA11yValue = ( instance: ReactTestInstance -): ((value: A11yValue) => Array) => - function queryAllByA11yValueFn(value) { - return instance.findAll( +): (( + value: A11yValue, + queryOptions?: CommonQueryOptions +) => Array) => + function queryAllByA11yValueFn(value, queryOptions) { + return findAll( + instance, (node) => typeof node.type === 'string' && - matchObjectProp(node.props.accessibilityValue, value) + matchObjectProp(node.props.accessibilityValue, value), + queryOptions ); }; @@ -40,19 +47,19 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( ); export type ByA11yValueQueries = { - getByA11yValue: GetByQuery; - getAllByA11yValue: GetAllByQuery; - queryByA11yValue: QueryByQuery; - queryAllByA11yValue: QueryAllByQuery; - findByA11yValue: FindByQuery; - findAllByA11yValue: FindAllByQuery; + getByA11yValue: GetByQuery; + getAllByA11yValue: GetAllByQuery; + queryByA11yValue: QueryByQuery; + queryAllByA11yValue: QueryAllByQuery; + findByA11yValue: FindByQuery; + findAllByA11yValue: FindAllByQuery; - getByAccessibilityValue: GetByQuery; - getAllByAccessibilityValue: GetAllByQuery; - queryByAccessibilityValue: QueryByQuery; - queryAllByAccessibilityValue: QueryAllByQuery; - findByAccessibilityValue: FindByQuery; - findAllByAccessibilityValue: FindAllByQuery; + getByAccessibilityValue: GetByQuery; + getAllByAccessibilityValue: GetAllByQuery; + queryByAccessibilityValue: QueryByQuery; + queryAllByAccessibilityValue: QueryAllByQuery; + findByAccessibilityValue: FindByQuery; + findAllByAccessibilityValue: FindAllByQuery; }; export const bindByA11yValueQueries = ( diff --git a/src/queries/displayValue.ts b/src/queries/displayValue.ts index 10f93600e..7c5ccc159 100644 --- a/src/queries/displayValue.ts +++ b/src/queries/displayValue.ts @@ -1,6 +1,7 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { TextInput } from 'react-native'; import { filterNodeByType } from '../helpers/filterNodeByType'; +import { findAll } from '../helpers/findAll'; import { matches, TextMatch } from '../matches'; import { makeQueries } from './makeQueries'; import type { @@ -11,7 +12,9 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; -import type { TextMatchOptions } from './text'; +import type { CommonQueryOptions, TextMatchOptions } from './options'; + +type ByDisplayValueOptions = CommonQueryOptions & TextMatchOptions; const getTextInputNodeByDisplayValue = ( node: ReactTestInstance, @@ -31,11 +34,14 @@ const queryAllByDisplayValue = ( instance: ReactTestInstance ): (( displayValue: TextMatch, - queryOptions?: TextMatchOptions + queryOptions?: ByDisplayValueOptions ) => Array) => function queryAllByDisplayValueFn(displayValue, queryOptions) { - return instance.findAll((node) => - getTextInputNodeByDisplayValue(node, displayValue, queryOptions) + return findAll( + instance, + (node) => + getTextInputNodeByDisplayValue(node, displayValue, queryOptions), + queryOptions ); }; @@ -51,12 +57,12 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( ); export type ByDisplayValueQueries = { - getByDisplayValue: GetByQuery; - getAllByDisplayValue: GetAllByQuery; - queryByDisplayValue: QueryByQuery; - queryAllByDisplayValue: QueryAllByQuery; - findByDisplayValue: FindByQuery; - findAllByDisplayValue: FindAllByQuery; + getByDisplayValue: GetByQuery; + getAllByDisplayValue: GetAllByQuery; + queryByDisplayValue: QueryByQuery; + queryAllByDisplayValue: QueryAllByQuery; + findByDisplayValue: FindByQuery; + findAllByDisplayValue: FindAllByQuery; }; export const bindByDisplayValueQueries = ( diff --git a/src/queries/hintText.ts b/src/queries/hintText.ts index bf9a33193..1d8bd26ef 100644 --- a/src/queries/hintText.ts +++ b/src/queries/hintText.ts @@ -1,4 +1,5 @@ import type { ReactTestInstance } from 'react-test-renderer'; +import { findAll } from '../helpers/findAll'; import { matches, TextMatch } from '../matches'; import { makeQueries } from './makeQueries'; import type { @@ -9,7 +10,9 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; -import { TextMatchOptions } from './text'; +import { CommonQueryOptions, TextMatchOptions } from './options'; + +type ByHintTextOptions = CommonQueryOptions & TextMatchOptions; const getNodeByHintText = ( node: ReactTestInstance, @@ -24,13 +27,15 @@ const queryAllByHintText = ( instance: ReactTestInstance ): (( hint: TextMatch, - queryOptions?: TextMatchOptions + queryOptions?: ByHintTextOptions ) => Array) => function queryAllByA11yHintFn(hint, queryOptions) { - return instance.findAll( + return findAll( + instance, (node) => typeof node.type === 'string' && - getNodeByHintText(node, hint, queryOptions) + getNodeByHintText(node, hint, queryOptions), + queryOptions ); }; @@ -46,28 +51,28 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( ); export type ByHintTextQueries = { - getByHintText: GetByQuery; - getAllByHintText: GetAllByQuery; - queryByHintText: QueryByQuery; - queryAllByHintText: QueryAllByQuery; - findByHintText: FindByQuery; - findAllByHintText: FindAllByQuery; + getByHintText: GetByQuery; + getAllByHintText: GetAllByQuery; + queryByHintText: QueryByQuery; + queryAllByHintText: QueryAllByQuery; + findByHintText: FindByQuery; + findAllByHintText: FindAllByQuery; // a11yHint aliases - getByA11yHint: GetByQuery; - getAllByA11yHint: GetAllByQuery; - queryByA11yHint: QueryByQuery; - queryAllByA11yHint: QueryAllByQuery; - findByA11yHint: FindByQuery; - findAllByA11yHint: FindAllByQuery; + getByA11yHint: GetByQuery; + getAllByA11yHint: GetAllByQuery; + queryByA11yHint: QueryByQuery; + queryAllByA11yHint: QueryAllByQuery; + findByA11yHint: FindByQuery; + findAllByA11yHint: FindAllByQuery; // accessibilityHint aliases - getByAccessibilityHint: GetByQuery; - getAllByAccessibilityHint: GetAllByQuery; - queryByAccessibilityHint: QueryByQuery; - queryAllByAccessibilityHint: QueryAllByQuery; - findByAccessibilityHint: FindByQuery; - findAllByAccessibilityHint: FindAllByQuery; + getByAccessibilityHint: GetByQuery; + getAllByAccessibilityHint: GetAllByQuery; + queryByAccessibilityHint: QueryByQuery; + queryAllByAccessibilityHint: QueryAllByQuery; + findByAccessibilityHint: FindByQuery; + findAllByAccessibilityHint: FindAllByQuery; }; export const bindByHintTextQueries = ( diff --git a/src/queries/labelText.ts b/src/queries/labelText.ts index 2af6ede4f..2677e603f 100644 --- a/src/queries/labelText.ts +++ b/src/queries/labelText.ts @@ -1,4 +1,5 @@ import type { ReactTestInstance } from 'react-test-renderer'; +import { findAll } from '../helpers/findAll'; import { matches, TextMatch } from '../matches'; import { makeQueries } from './makeQueries'; import type { @@ -9,7 +10,9 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; -import { TextMatchOptions } from './text'; +import { CommonQueryOptions, TextMatchOptions } from './options'; + +type ByLabelTextOptions = CommonQueryOptions & TextMatchOptions; const getNodeByLabelText = ( node: ReactTestInstance, @@ -24,13 +27,15 @@ const queryAllByLabelText = ( instance: ReactTestInstance ): (( text: TextMatch, - queryOptions?: TextMatchOptions + queryOptions?: ByLabelTextOptions ) => Array) => - function queryAllByLabelTextFn(text, queryOptions?: TextMatchOptions) { - return instance.findAll( + function queryAllByLabelTextFn(text, queryOptions) { + return findAll( + instance, (node) => typeof node.type === 'string' && - getNodeByLabelText(node, text, queryOptions) + getNodeByLabelText(node, text, queryOptions), + queryOptions ); }; @@ -46,12 +51,12 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( ); export type ByLabelTextQueries = { - getByLabelText: GetByQuery; - getAllByLabelText: GetAllByQuery; - queryByLabelText: QueryByQuery; - queryAllByLabelText: QueryAllByQuery; - findByLabelText: FindByQuery; - findAllByLabelText: FindAllByQuery; + getByLabelText: GetByQuery; + getAllByLabelText: GetAllByQuery; + queryByLabelText: QueryByQuery; + queryAllByLabelText: QueryAllByQuery; + findByLabelText: FindByQuery; + findAllByLabelText: FindAllByQuery; }; export const bindByLabelTextQueries = ( diff --git a/src/queries/options.ts b/src/queries/options.ts new file mode 100644 index 000000000..8687b26e4 --- /dev/null +++ b/src/queries/options.ts @@ -0,0 +1,8 @@ +import { NormalizerFn } from '../matches'; + +export type CommonQueryOptions = { hidden?: boolean }; + +export type TextMatchOptions = { + exact?: boolean; + normalizer?: NormalizerFn; +}; diff --git a/src/queries/placeholderText.ts b/src/queries/placeholderText.ts index dc4fa4d98..05dbcdfef 100644 --- a/src/queries/placeholderText.ts +++ b/src/queries/placeholderText.ts @@ -1,5 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { TextInput } from 'react-native'; +import { findAll } from '../helpers/findAll'; import { filterNodeByType } from '../helpers/filterNodeByType'; import { matches, TextMatch } from '../matches'; import { makeQueries } from './makeQueries'; @@ -11,7 +12,9 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; -import type { TextMatchOptions } from './text'; +import type { CommonQueryOptions, TextMatchOptions } from './options'; + +type ByPlaceholderTextOptions = CommonQueryOptions & TextMatchOptions; const getTextInputNodeByPlaceholderText = ( node: ReactTestInstance, @@ -29,11 +32,14 @@ const queryAllByPlaceholderText = ( instance: ReactTestInstance ): (( placeholder: TextMatch, - queryOptions?: TextMatchOptions + queryOptions?: ByPlaceholderTextOptions ) => Array) => function queryAllByPlaceholderFn(placeholder, queryOptions) { - return instance.findAll((node) => - getTextInputNodeByPlaceholderText(node, placeholder, queryOptions) + return findAll( + instance, + (node) => + getTextInputNodeByPlaceholderText(node, placeholder, queryOptions), + queryOptions ); }; @@ -49,12 +55,15 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( ); export type ByPlaceholderTextQueries = { - getByPlaceholderText: GetByQuery; - getAllByPlaceholderText: GetAllByQuery; - queryByPlaceholderText: QueryByQuery; - queryAllByPlaceholderText: QueryAllByQuery; - findByPlaceholderText: FindByQuery; - findAllByPlaceholderText: FindAllByQuery; + getByPlaceholderText: GetByQuery; + getAllByPlaceholderText: GetAllByQuery; + queryByPlaceholderText: QueryByQuery; + queryAllByPlaceholderText: QueryAllByQuery< + TextMatch, + ByPlaceholderTextOptions + >; + findByPlaceholderText: FindByQuery; + findAllByPlaceholderText: FindAllByQuery; }; export const bindByPlaceholderTextQueries = ( diff --git a/src/queries/role.ts b/src/queries/role.ts index 00d42b5e0..be13d474d 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -1,6 +1,7 @@ import { type AccessibilityState } from 'react-native'; import type { ReactTestInstance } from 'react-test-renderer'; import { accessibilityStateKeys } from '../helpers/accessiblity'; +import { findAll } from '../helpers/findAll'; import { matchAccessibilityState } from '../helpers/matchers/accessibilityState'; import { matchStringProp } from '../helpers/matchers/matchStringProp'; import type { TextMatch } from '../matches'; @@ -14,10 +15,12 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; +import { CommonQueryOptions } from './options'; -type ByRoleOptions = { - name?: TextMatch; -} & AccessibilityState; +type ByRoleOptions = CommonQueryOptions & + AccessibilityState & { + name?: TextMatch; + }; const matchAccessibleNameIfNeeded = ( node: ReactTestInstance, @@ -42,14 +45,16 @@ const queryAllByRole = ( instance: ReactTestInstance ): ((role: TextMatch, options?: ByRoleOptions) => Array) => function queryAllByRoleFn(role, options) { - return instance.findAll( + return findAll( + instance, (node) => // run the cheapest checks first, and early exit too avoid unneeded computations typeof node.type === 'string' && matchStringProp(node.props.accessibilityRole, role) && matchAccessibleStateIfNeeded(node, options) && - matchAccessibleNameIfNeeded(node, options?.name) + matchAccessibleNameIfNeeded(node, options?.name), + options ); }; diff --git a/src/queries/testId.ts b/src/queries/testId.ts index aec86511f..31d279763 100644 --- a/src/queries/testId.ts +++ b/src/queries/testId.ts @@ -1,4 +1,5 @@ import type { ReactTestInstance } from 'react-test-renderer'; +import { findAll } from '../helpers/findAll'; import { matches, TextMatch } from '../matches'; import { makeQueries } from './makeQueries'; import type { @@ -9,7 +10,9 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; -import type { TextMatchOptions } from './text'; +import type { CommonQueryOptions, TextMatchOptions } from './options'; + +type ByTestIdOptions = CommonQueryOptions & TextMatchOptions; const getNodeByTestId = ( node: ReactTestInstance, @@ -24,14 +27,16 @@ const queryAllByTestId = ( instance: ReactTestInstance ): (( testId: TextMatch, - queryOptions?: TextMatchOptions + queryOptions?: ByTestIdOptions ) => Array) => function queryAllByTestIdFn(testId, queryOptions) { - const results = instance - .findAll((node) => getNodeByTestId(node, testId, queryOptions)) - .filter((element) => typeof element.type === 'string'); + const results = findAll( + instance, + (node) => getNodeByTestId(node, testId, queryOptions), + queryOptions + ); - return results; + return results.filter((element) => typeof element.type === 'string'); }; const getMultipleError = (testId: TextMatch) => @@ -46,12 +51,12 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( ); export type ByTestIdQueries = { - getByTestId: GetByQuery; - getAllByTestId: GetAllByQuery; - queryByTestId: QueryByQuery; - queryAllByTestId: QueryAllByQuery; - findByTestId: FindByQuery; - findAllByTestId: FindAllByQuery; + getByTestId: GetByQuery; + getAllByTestId: GetAllByQuery; + queryByTestId: QueryByQuery; + queryAllByTestId: QueryAllByQuery; + findByTestId: FindByQuery; + findAllByTestId: FindAllByQuery; }; export const bindByTestIdQueries = ( diff --git a/src/queries/text.ts b/src/queries/text.ts index 9633c9a61..f030cc9cb 100644 --- a/src/queries/text.ts +++ b/src/queries/text.ts @@ -1,13 +1,13 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { Text } from 'react-native'; import * as React from 'react'; -import { filterNodeByType } from '../helpers/filterNodeByType'; import { isHostElementForType, getCompositeParentOfType, } from '../helpers/component-tree'; +import { filterNodeByType } from '../helpers/filterNodeByType'; +import { findAll } from '../helpers/findAll'; import { matches, TextMatch } from '../matches'; -import type { NormalizerFn } from '../matches'; import { makeQueries } from './makeQueries'; import type { FindAllByQuery, @@ -17,11 +17,9 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; +import { CommonQueryOptions, TextMatchOptions } from './options'; -export type TextMatchOptions = { - exact?: boolean; - normalizer?: NormalizerFn; -}; +type ByTextOptions = CommonQueryOptions & TextMatchOptions; const getChildrenAsText = (children: React.ReactChild[]) => { const textContent: string[] = []; @@ -57,7 +55,7 @@ const getChildrenAsText = (children: React.ReactChild[]) => { const getNodeByText = ( node: ReactTestInstance, text: TextMatch, - options: TextMatchOptions = {} + options: ByTextOptions = {} ) => { const isTextComponent = filterNodeByType(node, Text); if (isTextComponent) { @@ -73,10 +71,7 @@ const getNodeByText = ( const queryAllByText = ( instance: ReactTestInstance -): (( - text: TextMatch, - options?: TextMatchOptions -) => Array) => +): ((text: TextMatch, options?: ByTextOptions) => Array) => function queryAllByTextFn(text, options) { const baseInstance = isHostElementForType(instance, Text) ? getCompositeParentOfType(instance, Text) @@ -86,8 +81,10 @@ const queryAllByText = ( return []; } - const results = baseInstance.findAll((node) => - getNodeByText(node, text, options) + const results = findAll( + baseInstance, + (node) => getNodeByText(node, text, options), + options ); return results; @@ -105,12 +102,12 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( ); export type ByTextQueries = { - getByText: GetByQuery; - getAllByText: GetAllByQuery; - queryByText: QueryByQuery; - queryAllByText: QueryAllByQuery; - findByText: FindByQuery; - findAllByText: FindAllByQuery; + getByText: GetByQuery; + getAllByText: GetAllByQuery; + queryByText: QueryByQuery; + queryAllByText: QueryAllByQuery; + findByText: FindByQuery; + findAllByText: FindAllByQuery; }; export const bindByTextQueries = ( diff --git a/typings/index.flow.js b/typings/index.flow.js index a1a27a20b..a639c13a1 100644 --- a/typings/index.flow.js +++ b/typings/index.flow.js @@ -8,6 +8,7 @@ type QueryAllReturn = Array | []; type FindReturn = Promise; type FindAllReturn = Promise; +type CommonQueryOptions = { hidden?: boolean }; type TextMatch = string | RegExp; declare type NormalizerFn = (textToNormalize: string) => string; @@ -74,109 +75,123 @@ type WaitForFunction = ( options?: WaitForOptions ) => Promise; +type ByTextOptions = CommonQueryOptions & TextMatchOptions; + interface ByTextQueries { - getByText: (text: TextMatch, options?: TextMatchOptions) => ReactTestInstance; + getByText: (text: TextMatch, options?: ByTextOptions) => ReactTestInstance; getAllByText: ( text: TextMatch, - options?: TextMatchOptions + options?: ByTextOptions ) => Array; queryByText: ( name: TextMatch, - options?: TextMatchOptions + options?: ByTextOptions ) => ReactTestInstance | null; queryAllByText: ( text: TextMatch, - options?: TextMatchOptions + options?: ByTextOptions ) => Array | []; findByText: ( text: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByTextOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByText: ( text: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByTextOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; } +type ByTestIdOptions = CommonQueryOptions & TextMatchOptions; + interface ByTestIdQueries { getByTestId: ( testID: TextMatch, - options?: TextMatchOptions + options?: ByTestIdOptions ) => ReactTestInstance; getAllByTestId: ( testID: TextMatch, - options?: TextMatchOptions + options?: ByTestIdOptions ) => Array; - queryByTestId: (testID: TextMatch) => ReactTestInstance | null; - queryAllByTestId: (testID: TextMatch) => Array | []; + queryByTestId: ( + testID: TextMatch, + options?: ByTestIdOptions + ) => ReactTestInstance | null; + queryAllByTestId: ( + testID: TextMatch, + options?: ByTestIdOptions + ) => Array | []; findByTestId: ( testID: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByTestIdOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByTestId: ( testID: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByTestIdOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; } +type ByDisplayValueOptions = CommonQueryOptions & TextMatchOptions; + interface ByDisplayValueQueries { getByDisplayValue: ( value: TextMatch, - options?: TextMatchOptions + options?: ByDisplayValueOptions ) => ReactTestInstance; getAllByDisplayValue: ( value: TextMatch, - options?: TextMatchOptions + options?: ByDisplayValueOptions ) => Array; queryByDisplayValue: ( value: TextMatch, - options?: TextMatchOptions + options?: ByDisplayValueOptions ) => ReactTestInstance | null; queryAllByDisplayValue: ( value: TextMatch, - options?: TextMatchOptions + options?: ByDisplayValueOptions ) => Array | []; findByDisplayValue: ( value: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByDisplayValueOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByDisplayValue: ( value: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByDisplayValueOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; } +type ByPlaceholderTextOptions = CommonQueryOptions & TextMatchOptions; + interface ByPlaceholderTextQueries { getByPlaceholderText: ( placeholder: TextMatch, - options?: TextMatchOptions + options?: ByPlaceholderTextOptions ) => ReactTestInstance; getAllByPlaceholderText: ( placeholder: TextMatch, - options?: TextMatchOptions + options?: ByPlaceholderTextOptions ) => Array; queryByPlaceholderText: ( placeholder: TextMatch, - options?: TextMatchOptions + options?: ByPlaceholderTextOptions ) => ReactTestInstance | null; queryAllByPlaceholderText: ( placeholder: TextMatch, - options?: TextMatchOptions + options?: ByPlaceholderTextOptions ) => Array | []; findByPlaceholderText: ( placeholder: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByPlaceholderTextOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByPlaceholderText: ( placeholder: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByPlaceholderTextOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; } @@ -203,82 +218,88 @@ interface UnsafeByPropsQueries { | []; } -type ByRoleOptions = { +type ByRoleOptions = CommonQueryOptions & { ...A11yState, name?: string, }; +type ByLabelTextOptions = CommonQueryOptions & TextMatchOptions; +type ByHintTextOptions = CommonQueryOptions & TextMatchOptions; + interface A11yAPI { // Label - getByLabelText: (matcher: TextMatch, options?: TextMatchOptions) => GetReturn; + getByLabelText: ( + matcher: TextMatch, + options?: ByLabelTextOptions + ) => GetReturn; getAllByLabelText: ( matcher: TextMatch, - options?: TextMatchOptions + options?: ByLabelTextOptions ) => GetAllReturn; queryByLabelText: ( matcher: TextMatch, - options?: TextMatchOptions + options?: ByLabelTextOptions ) => QueryReturn; queryAllByLabelText: ( matcher: TextMatch, - options?: TextMatchOptions + options?: ByLabelTextOptions ) => QueryAllReturn; findByLabelText: ( matcher: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByLabelTextOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByLabelText: ( matcher: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByLabelTextOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; // Hint - getByA11yHint: (matcher: TextMatch, options?: TextMatchOptions) => GetReturn; - getByHintText: (matcher: TextMatch, options?: TextMatchOptions) => GetReturn; + getByA11yHint: (matcher: TextMatch, options?: ByHintTextOptions) => GetReturn; + getByHintText: (matcher: TextMatch, options?: ByHintTextOptions) => GetReturn; getAllByA11yHint: ( matcher: TextMatch, - options?: TextMatchOptions + options?: ByHintTextOptions ) => GetAllReturn; getAllByHintText: ( matcher: TextMatch, - options?: TextMatchOptions + options?: ByHintTextOptions ) => GetAllReturn; queryByA11yHint: ( matcher: TextMatch, - options?: TextMatchOptions + options?: ByHintTextOptions ) => QueryReturn; queryByHintText: ( matcher: TextMatch, - options?: TextMatchOptions + options?: ByHintTextOptions ) => QueryReturn; queryAllByA11yHint: ( matcher: TextMatch, - options?: TextMatchOptions + options?: ByHintTextOptions ) => QueryAllReturn; queryAllByHintText: ( matcher: TextMatch, - options?: TextMatchOptions + options?: ByHintTextOptions ) => QueryAllReturn; findByA11yHint: ( matcher: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByHintTextOptions, waitForOptions?: WaitForOptions ) => FindReturn; findByHintText: ( matcher: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByHintTextOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByA11yHint: ( matcher: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByHintTextOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; findAllByHintText: ( matcher: TextMatch, - queryOptions?: TextMatchOptions, + queryOptions?: ByHintTextOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; @@ -308,30 +329,58 @@ interface A11yAPI { ) => FindAllReturn; // State - getByA11yState: (matcher: A11yState) => GetReturn; - getAllByA11yState: (matcher: A11yState) => GetAllReturn; - queryByA11yState: (matcher: A11yState) => QueryReturn; - queryAllByA11yState: (matcher: A11yState) => QueryAllReturn; + getByA11yState: ( + matcher: A11yState, + options?: CommonQueryOptions + ) => GetReturn; + getAllByA11yState: ( + matcher: A11yState, + options?: CommonQueryOptions + ) => GetAllReturn; + queryByA11yState: ( + matcher: A11yState, + options?: CommonQueryOptions + ) => QueryReturn; + queryAllByA11yState: ( + matcher: A11yState, + options?: CommonQueryOptions + ) => QueryAllReturn; findByA11yState: ( matcher: A11yState, + queryOptions?: CommonQueryOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByA11yState: ( matcher: A11yState, + queryOptions?: CommonQueryOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; // Value - getByA11yValue: (matcher: A11yValue) => GetReturn; - getAllByA11yValue: (matcher: A11yValue) => GetAllReturn; - queryByA11yValue: (matcher: A11yValue) => QueryReturn; - queryAllByA11yValue: (matcher: A11yValue) => QueryAllReturn; + getByA11yValue: ( + matcher: A11yValue, + options?: CommonQueryOptions + ) => GetReturn; + getAllByA11yValue: ( + matcher: A11yValue, + options?: CommonQueryOptions + ) => GetAllReturn; + queryByA11yValue: ( + matcher: A11yValue, + options?: CommonQueryOptions + ) => QueryReturn; + queryAllByA11yValue: ( + matcher: A11yValue, + options?: CommonQueryOptions + ) => QueryAllReturn; findByA11yValue: ( matcher: A11yValue, + queryOptions?: CommonQueryOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByA11yValue: ( matcher: A11yValue, + queryOptions?: CommonQueryOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; } diff --git a/website/docs/API.md b/website/docs/API.md index 1591a92e0..d7151b1cf 100644 --- a/website/docs/API.md +++ b/website/docs/API.md @@ -50,6 +50,7 @@ title: API - [Configuration](#configuration) - [`configure`](#configure) - [`asyncUtilTimeout` option](#asyncutiltimeout-option) + - [`defaultHidden` option](#defaulthidden-option) - [`defaultDebugOptions` option](#defaultdebugoptions-option) - [`resetToDefaults()`](#resettodefaults) - [Environment variables](#environment-variables) @@ -69,7 +70,7 @@ Defined as: ```jsx function render( component: React.Element, - options?: RenderOptions, + options?: RenderOptions ): RenderResult {} ``` @@ -92,7 +93,7 @@ test('should verify two questions', () => { The `render` method returns a `RenderResult` object having properties described below. :::info -Latest `render` result is kept in [`screen`](#screen) variable that can be imported from `@testing-library/react-native` package. +Latest `render` result is kept in [`screen`](#screen) variable that can be imported from `@testing-library/react-native` package. Using `screen` instead of destructuring `render` result is recommended approach. See [this article](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-screen) from Kent C. Dodds for more details. ::: @@ -124,10 +125,10 @@ unstable_validateStringsRenderedWithinText?: boolean; ``` :::note -This options is experimental, in some cases it might not work as intended, and its behavior might change without observing [SemVer](https://semver.org/) requirements for breaking changes. +This options is experimental, in some cases it might not work as intended, and its behavior might change without observing [SemVer](https://semver.org/) requirements for breaking changes. ::: -This **experimental** option allows you to replicate React Native behavior of throwing `Invariant Violation: Text strings must be rendered within a component` error when you try to render `string` value under components different than ``, e.g. under ``. +This **experimental** option allows you to replicate React Native behavior of throwing `Invariant Violation: Text strings must be rendered within a component` error when you try to render `string` value under components different than ``, e.g. under ``. This check is not enforced by React Test Renderer and hence by default React Native Testing Library also does not check this. That might result in runtime errors when running your code on a device, while the code works without errors in tests. @@ -173,7 +174,7 @@ Usually you should not need to call `unmount` as it is done automatically if you ### `debug` ```ts -interface DebugOptions { +interface DebugOptions { message?: string; mapProps?: MapPropsFunction; } @@ -181,7 +182,7 @@ interface DebugOptions { debug(options?: DebugOptions | string): void ``` -Pretty prints deeply rendered component passed to `render`. +Pretty prints deeply rendered component passed to `render`. #### `message` option @@ -204,39 +205,37 @@ optional message ``` - #### `mapProps` option -You can use the `mapProps` option to transform the props that will be printed : +You can use the `mapProps` option to transform the props that will be printed : ```jsx -render(); -debug({ mapProps : ({ style, ...props }) => ({ props }) }) +render(); +debug({ mapProps: ({ style, ...props }) => ({ props }) }); ``` -This will log the rendered JSX without the `style` props. +This will log the rendered JSX without the `style` props. The `children` prop cannot be filtered out so the following will print all rendered components with all props but `children` filtered out. - ```ts -debug({ mapProps : props => ({}) }) +debug({ mapProps: (props) => ({}) }); ``` This option can be used to target specific props when debugging a query (for instance keeping only `children` prop when debugging a `getByText` query). - You can also transform prop values so that they are more readable (e.g. flatten styles). +You can also transform prop values so that they are more readable (e.g. flatten styles). - ```ts +```ts import { StyleSheet } from 'react-native'; debug({ mapProps : {({ style, ...props })} => ({ style : StyleSheet.flatten(style), ...props }) }); - ``` +``` Or remove props that have little value when debugging tests, e.g. path prop for svgs ```ts -debug({ mapProps : ({ path, ...props }) => ({ ...props })}); +debug({ mapProps: ({ path, ...props }) => ({ ...props }) }); ``` #### `debug.shallow` @@ -265,15 +264,15 @@ A reference to the rendered root element. let screen: RenderResult; ``` -Hold the value of latest render call for easier access to query and other functions returned by [`render`](#render). +Hold the value of latest render call for easier access to query and other functions returned by [`render`](#render). Its value is automatically cleared after each test by calling [`cleanup`](#cleanup). If no `render` call has been made in a given test then it holds a special object that implements `RenderResult` but throws a helpful error on each property and method access. -This can also be used to build test utils that would normally require to be in render scope, either in a test file or globally for your project. For instance: +This can also be used to build test utils that would normally require to be in render scope, either in a test file or globally for your project. For instance: ```ts // Prints the rendered components omitting all props except children. -const debugText = () => screen.debug({ mapProps : props => ({}) }) +const debugText = () => screen.debug({ mapProps: (props) => ({}) }); ``` ## `cleanup` @@ -556,7 +555,11 @@ function waitForElementToBeRemoved( Waits for non-deterministic periods of time until queried element is removed or times out. `waitForElementToBeRemoved` periodically calls `expectation` every `interval` milliseconds to determine whether the element has been removed or not. ```jsx -import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native'; +import { + render, + screen, + waitForElementToBeRemoved, +} from '@testing-library/react-native'; test('waiting for an Banana to be removed', async () => { render(); @@ -584,13 +587,9 @@ If you receive warnings related to `act()` function consult our [Undestanding Ac Defined as: ```jsx -function within( - element: ReactTestInstance -): Queries {} +function within(element: ReactTestInstance): Queries {} -function getQueriesForElement( - element: ReactTestInstance -): Queries {} +function getQueriesForElement(element: ReactTestInstance): Queries {} ``` `within` (also available as `getQueriesForElement` alias) performs [queries](./Queries.md) scoped to given element. @@ -774,7 +773,6 @@ it('should use context value', () => { }); ``` - ## Configuration ### `configure` @@ -782,15 +780,21 @@ it('should use context value', () => { ```ts type Config = { asyncUtilTimeout: number; - defaultDebugOptions: Partial + defaultHidden: boolean; + defaultDebugOptions: Partial; }; -function configure(options: Partial) {} +function configure(options: Partial) {} ``` + #### `asyncUtilTimeout` option Default timeout, in ms, for async helper functions (`waitFor`, `waitForElementToBeRemoved`) and `findBy*` queries. Defaults to 1000 ms. +#### `defaultHidden` option + +Default [hidden](Queries.md#hidden-option) query option for all queries. This default option will be overridden by the one you specify directly when using your query. + #### `defaultDebugOptions` option Default [debug options](#debug) to be used when calling `debug()`. These default options will be overridden by the ones you specify directly when calling `debug()`. @@ -804,6 +808,7 @@ function resetToDefaults() {} ### Environment variables #### `RNTL_SKIP_AUTO_CLEANUP` + Set to `true` to disable automatic `cleanup()` after each test. It works the same as importing `react-native-testing-library/dont-cleanup-after-each` or using `react-native-testing-library/pure`. ```shell @@ -811,6 +816,7 @@ $ RNTL_SKIP_AUTO_CLEANUP=true jest ``` #### `RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS` + Set to `true` to disable auto-detection of fake timers. This might be useful in rare cases when you want to use non-Jest fake timers. See [issue #886](https://github.com/callstack/react-native-testing-library/issues/886) for more details. ```shell @@ -822,23 +828,22 @@ $ RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS=true jest ### `isInaccessible` ```ts -function isInaccessible( - element: ReactTestInstance | null -): boolean {} +function isInaccessible(element: ReactTestInstance | null): boolean {} ``` -Checks if given element is hidden from assistive technology, e.g. screen readers. +Checks if given element is hidden from assistive technology, e.g. screen readers. :::note -Like [`isInaccessible`](https://testing-library.com/docs/dom-testing-library/api-accessibility/#isinaccessible) function from [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro) this function considers both accessibility elements and presentational elements (regular `View`s) to be accessible, unless they are hidden in terms of host platform. +Like [`isInaccessible`](https://testing-library.com/docs/dom-testing-library/api-accessibility/#isinaccessible) function from [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro) this function considers both accessibility elements and presentational elements (regular `View`s) to be accessible, unless they are hidden in terms of host platform. This covers only part of [ARIA notion of Accessiblity Tree](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), as ARIA excludes both hidden and presentational elements from the Accessibility Tree. ::: -For the scope of this function, element is inaccessible when it, or any of its ancestors, meets any of the following conditions: - * it has `display: none` style - * it has [`accessibilityElementsHidden`](https://reactnative.dev/docs/accessibility#accessibilityelementshidden-ios) prop set to `true` - * it has [`importantForAccessibility`](https://reactnative.dev/docs/accessibility#importantforaccessibility-android) prop set to `no-hide-descendants` - * it has sibling host element with [`accessibilityViewIsModal`](https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios) prop set to `true` - +For the scope of this function, element is inaccessible when it, or any of its ancestors, meets any of the following conditions: + +- it has `display: none` style +- it has [`accessibilityElementsHidden`](https://reactnative.dev/docs/accessibility#accessibilityelementshidden-ios) prop set to `true` +- it has [`importantForAccessibility`](https://reactnative.dev/docs/accessibility#importantforaccessibility-android) prop set to `no-hide-descendants` +- it has sibling host element with [`accessibilityViewIsModal`](https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios) prop set to `true` + Specifying `accessible={false}`, `accessiblityRole="none"`, or `importantForAccessibility="no"` props does not cause the element to become inaccessible. diff --git a/website/docs/EslintPLluginTestingLibrary.md b/website/docs/EslintPLluginTestingLibrary.md index 08cfe4746..0a75d94ee 100644 --- a/website/docs/EslintPLluginTestingLibrary.md +++ b/website/docs/EslintPLluginTestingLibrary.md @@ -3,7 +3,6 @@ id: eslint-plugin-testing-library title: ESLint Plugin Testing Library Compatibility --- - Most of the rules of the [eslint-plugin-testing-library](https://github.com/testing-library/eslint-plugin-testing-library) are compatible with this library except the following: - [prefer-user-event](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-user-event.md): `userEvent` requires a DOM environment so it is not compatible with this library @@ -26,4 +25,4 @@ To get the rule [consistent-data-testid](https://github.com/testing-library/esli } ] } -``` \ No newline at end of file +``` diff --git a/website/docs/FAQ.md b/website/docs/FAQ.md index 96001a5bc..bff9d29d6 100644 --- a/website/docs/FAQ.md +++ b/website/docs/FAQ.md @@ -11,7 +11,7 @@ title: FAQ React Native Testing Library does not provide a full React Native runtime since that would require running on physical device or iOS simulator/Android emulator to provision the underlying OS and platform APIs. -Instead of using React Native renderer, it simulates only the JavaScript part of its runtime by +Instead of using React Native renderer, it simulates only the JavaScript part of its runtime by using [React Test Renderer](https://reactjs.org/docs/test-renderer.html) while providing queries and `fireEvent` APIs that mimick certain behaviors from the real runtime. diff --git a/website/docs/GettingStarted.md b/website/docs/GettingStarted.md index 2110ec98c..6391ba9e0 100644 --- a/website/docs/GettingStarted.md +++ b/website/docs/GettingStarted.md @@ -91,8 +91,8 @@ test('form submits two answers', () => { fireEvent.press(screen.getByText('Submit')); expect(mockFn).toBeCalledWith({ - '1': { q: 'q1', a: 'a1' }, - '2': { q: 'q2', a: 'a2' }, + 1: { q: 'q1', a: 'a1' }, + 2: { q: 'q2', a: 'a2' }, }); }); ``` diff --git a/website/docs/Queries.md b/website/docs/Queries.md index 733e7dcf0..430311c6f 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -26,6 +26,8 @@ title: Queries - [Default state for: `disabled`, `selected`, and `busy` keys](#default-state-for-disabled-selected-and-busy-keys) - [Default state for: `checked` and `expanded` keys](#default-state-for-checked-and-expanded-keys) - [`ByA11Value`, `ByAccessibilityValue`](#bya11value-byaccessibilityvalue) +- [Common options](#common-options) + - [`hidden` option](#hidden-option) - [TextMatch](#textmatch) - [Examples](#examples) - [Precision](#precision) @@ -87,7 +89,7 @@ type ReactTestInstance = { ### Options -Usually query first argument can be a **string** or a **regex**. Some queries accept optional argument which change string matching behaviour. See [TextMatch](#textmatch) for more info. +Usually query first argument can be a **string** or a **regex**. All queries take at least the [`hidden`](#hidden-option) option as an optionnal second argument and some queries accept more options which change string matching behaviour. See [TextMatch](#textmatch) for more info. ### `ByText` @@ -99,6 +101,7 @@ getByText( options?: { exact?: boolean; normalizer?: (text: string) => string; + hidden?: boolean; } ): ReactTestInstance; ``` @@ -124,6 +127,7 @@ getByPlaceholderText( options?: { exact?: boolean; normalizer?: (text: string) => string; + hidden?: boolean; } ): ReactTestInstance; ``` @@ -147,6 +151,7 @@ getByDisplayValue( options?: { exact?: boolean; normalizer?: (text: string) => string; + hidden?: boolean; } ): ReactTestInstance; ``` @@ -170,6 +175,7 @@ getByTestId( options?: { exact?: boolean; normalizer?: (text: string) => string; + hidden?: boolean; } ): ReactTestInstance; ``` @@ -197,6 +203,7 @@ getByLabelText( options?: { exact?: boolean; normalizer?: (text: string) => string; + hidden?: boolean; } ): ReactTestInstance; ``` @@ -222,6 +229,7 @@ getByHintText( options?: { exact?: boolean; normalizer?: (text: string) => string; + hidden?: boolean; } ): ReactTestInstance; ``` @@ -246,13 +254,14 @@ Please consult [Apple guidelines on how `accessibilityHint` should be used](http ```ts getByRole( role: TextMatch, - option?: { + options?: { name?: TextMatch disabled?: boolean, selected?: boolean, checked?: boolean | 'mixed', busy?: boolean, expanded?: boolean, + hidden?: boolean; } ): ReactTestInstance; ``` @@ -293,6 +302,9 @@ getByA11yState( checked?: boolean | 'mixed', expanded?: boolean, busy?: boolean, + }, + options?: { + hidden?: boolean; } ): ReactTestInstance; ``` @@ -310,22 +322,26 @@ const element = screen.getByA11yState({ disabled: true }); #### Default state for: `disabled`, `selected`, and `busy` keys -Passing `false` matcher value will match both elements with explicit `false` state value and without explicit state value. +Passing `false` matcher value will match both elements with explicit `false` state value and without explicit state value. For instance, `getByA11yState({ disabled: false })` will match elements with following props: -* `accessibilityState={{ disabled: false, ... }}` -* no `disabled` key under `accessibilityState` prop, e.g. `accessibilityState={{}}` -* no `accessibilityState` prop at all + +- `accessibilityState={{ disabled: false, ... }}` +- no `disabled` key under `accessibilityState` prop, e.g. `accessibilityState={{}}` +- no `accessibilityState` prop at all #### Default state for: `checked` and `expanded` keys + Passing `false` matcher value will only match elements with explicit `false` state value. For instance, `getByA11yState({ checked: false })` will only match elements with: -* `accessibilityState={{ checked: false, ... }}` + +- `accessibilityState={{ checked: false, ... }}` but will not match elements with following props: -* no `checked` key under `accessibilityState` prop, e.g. `accessibilityState={{}}` -* no `accessibilityState` prop at all + +- no `checked` key under `accessibilityState` prop, e.g. `accessibilityState={{}}` +- no `accessibilityState` prop at all The difference in handling default values is made to reflect observed accessibility behaviour on iOS and Android platforms. ::: @@ -342,6 +358,9 @@ getByA11yValue( max?: number; now?: number; text?: string; + }, + options?: { + hidden?: boolean; } ): ReactTestInstance; ``` @@ -355,6 +374,33 @@ render(); const element = screen.getByA11yValue({ min: 40 }); ``` +## Common options + +### `hidden` option + +All queries have the `hidden` option which enables them to respect accessibility props on components when it is set to `false`. If you set `hidden` to `true`, elements that are normally excluded from the accessibility tree are considered for the query as well. Currently `hidden` option is set `true` by default, which means that elements hidden from accessibility will be included by default. However, we plan to change the default value to `hidden: false` in the next major release. + +You can configure the default value with the [`configure` function](API.md#configure). + +An element is considered to be hidden from accessibility based on [`isInaccessible()`](./API.md#isinaccessible) function result. + +**Examples** + +```tsx +render(I am hidden from accessibility); + +// Ignore hidden elements +expect( + screen.queryByText('I am hidden from accessibility', { hidden: false }) +).toBeFalsy(); + +// Match hidden elements +expect(screen.getByText('I am hidden from accessibility')).toBeTruthy(); // Defaults to hidden: true for now +expect( + screen.getByText('I am hidden from accessibility', { hidden: true }) +).toBeTruthy(); +``` + ## TextMatch ```ts diff --git a/website/docs/TestingEnvironment.md b/website/docs/TestingEnvironment.md index 155c29420..5a5d09a4f 100644 --- a/website/docs/TestingEnvironment.md +++ b/website/docs/TestingEnvironment.md @@ -20,7 +20,7 @@ When you run your tests in React Native Testing Library, somewhat contrary to wh ### React Test Renderer -Instead, RNTL uses React Test Renderer which is a specialised renderer that allows rendering to pure JavaScript objects without access to mobile OS, and that can run in a Node.js environment using Jest (or any other JavaScript test runner). +Instead, RNTL uses React Test Renderer which is a specialised renderer that allows rendering to pure JavaScript objects without access to mobile OS, and that can run in a Node.js environment using Jest (or any other JavaScript test runner). Using React Test Renderer has pros and cons. @@ -137,7 +137,7 @@ You should avoid navigating over element tree, as this makes your testing code f When navigating a tree of react elements using `parent` or `children` props of a `ReactTestInstance` element, you will encounter both host and composite elements. You should be careful when navigating the element tree, as the tree structure for 3rd party components and change independently from your code and cause unexpected test failures. -Inside RNTL we have various tree navigation helpers: `getHostParent`, `getHostChildren`, etc. These are intentionally not exported as using them is not a recommended practice. +Inside RNTL we have various tree navigation helpers: `getHostParent`, `getHostChildren`, etc. These are intentionally not exported as using them is not a recommended practice. ### Queries diff --git a/website/docs/Troubleshooting.md b/website/docs/Troubleshooting.md index 2647b664b..1dde5c4b2 100644 --- a/website/docs/Troubleshooting.md +++ b/website/docs/Troubleshooting.md @@ -8,6 +8,7 @@ This guide describes common issues found by users when integrating React Native ## Matching React Native, React & React Test Renderer versions Check that you have matching versions of core dependencies: + - React Native - React - React Test Renderer @@ -19,6 +20,7 @@ React Test Renderer usually has same major & minor version as React, as they are Related issues: [#1061](https://github.com/callstack/react-native-testing-library/issues/1061), [#938](https://github.com/callstack/react-native-testing-library/issues/938), [#920](https://github.com/callstack/react-native-testing-library/issues/920) Errors that might indicate that you are facing this issue: + - `TypeError: Cannot read property 'current' of undefined` when calling `render()` - `TypeError: Cannot read property 'isBatchingLegacy' of undefined` when calling `render()` @@ -32,8 +34,8 @@ In case something does not work in your setup you can refer to this repository f When writing tests you may encounter warnings connected with `act()` function. There are two kinds of these warnings: -* sync `act()` warning - `Warning: An update to Component inside a test was not wrapped in act(...)` -* async `act()` warning - `Warning: You called act(async () => ...) without await` +- sync `act()` warning - `Warning: An update to Component inside a test was not wrapped in act(...)` +- async `act()` warning - `Warning: You called act(async () => ...) without await` You can read more about `act()` function in our [understanding `act` function guide](https://callstack.github.io/react-native-testing-library/docs/understanding-act).