diff --git a/.gitignore b/.gitignore index 13a6c4cc7..2f6d9c694 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +coverage *.log .eslintcache build diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 5879b8ee2..599e36da7 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -6,6 +6,7 @@ beforeEach(() => { test('getConfig() returns existing configuration', () => { expect(getConfig().asyncUtilTimeout).toEqual(1000); + expect(getConfig().defaultIncludeHiddenElements).toEqual(true); }); test('configure() overrides existing config values', () => { @@ -14,14 +15,26 @@ test('configure() overrides existing config values', () => { expect(getConfig()).toEqual({ asyncUtilTimeout: 5000, defaultDebugOptions: { message: 'debug message' }, - defaultHidden: true, + defaultIncludeHiddenElements: true, }); }); test('resetToDefaults() resets config to defaults', () => { - configure({ asyncUtilTimeout: 5000 }); + configure({ + asyncUtilTimeout: 5000, + defaultIncludeHiddenElements: false, + }); expect(getConfig().asyncUtilTimeout).toEqual(5000); + expect(getConfig().defaultIncludeHiddenElements).toEqual(false); resetToDefaults(); expect(getConfig().asyncUtilTimeout).toEqual(1000); + expect(getConfig().defaultIncludeHiddenElements).toEqual(true); +}); + +test('configure handles alias option defaultHidden', () => { + expect(getConfig().defaultIncludeHiddenElements).toEqual(true); + + configure({ defaultHidden: false }); + expect(getConfig().defaultIncludeHiddenElements).toEqual(false); }); diff --git a/src/config.ts b/src/config.ts index b8133c3b9..0f3a8c22f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,24 +4,37 @@ export type Config = { /** Default timeout, in ms, for `waitFor` and `findBy*` queries. */ asyncUtilTimeout: number; - /** Default hidden value for all queries */ - defaultHidden: boolean; + /** Default value for `includeHiddenElements` query option. */ + defaultIncludeHiddenElements: boolean; /** Default options for `debug` helper. */ defaultDebugOptions?: Partial; }; +export type ConfigAliasOptions = { + /** RTL-compatibility alias to `defaultIncludeHiddenElements` */ + defaultHidden: boolean; +}; + const defaultConfig: Config = { asyncUtilTimeout: 1000, - defaultHidden: true, + defaultIncludeHiddenElements: true, }; let config = { ...defaultConfig }; -export function configure(options: Partial) { +export function configure(options: Partial) { + const { defaultHidden, ...restOptions } = options; + + const defaultIncludeHiddenElements = + restOptions.defaultIncludeHiddenElements ?? + defaultHidden ?? + config.defaultIncludeHiddenElements; + config = { ...config, - ...options, + ...restOptions, + defaultIncludeHiddenElements, }; } diff --git a/src/helpers/__tests__/component-tree.test.tsx b/src/helpers/__tests__/component-tree.test.tsx index 0056f146a..39ce7252b 100644 --- a/src/helpers/__tests__/component-tree.test.tsx +++ b/src/helpers/__tests__/component-tree.test.tsx @@ -45,6 +45,10 @@ describe('getHostParent()', () => { expect(getHostParent(hostGrandparent)).toBe(null); }); + it('returns host parent for null', () => { + expect(getHostParent(null)).toBe(null); + }); + it('returns host parent for composite component', () => { const view = render( @@ -65,7 +69,7 @@ describe('getHostChildren()', () => { - + Hello ); @@ -74,6 +78,8 @@ describe('getHostChildren()', () => { expect(getHostChildren(hostSubject)).toEqual([]); const hostSibling = view.getByTestId('sibling'); + expect(getHostChildren(hostSibling)).toEqual([]); + const hostParent = view.getByTestId('parent'); expect(getHostChildren(hostParent)).toEqual([hostSubject, hostSibling]); diff --git a/src/helpers/__tests__/includeHiddenElements.test.tsx b/src/helpers/__tests__/includeHiddenElements.test.tsx new file mode 100644 index 000000000..cf6cd738c --- /dev/null +++ b/src/helpers/__tests__/includeHiddenElements.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { View } from 'react-native'; +import { configure, render, screen } from '../..'; + +test('includeHiddenElements query option takes priority over hidden option and global config', () => { + configure({ defaultHidden: true, defaultIncludeHiddenElements: true }); + render(); + expect( + screen.queryByTestId('view', { includeHiddenElements: false, hidden: true }) + ).toBeFalsy(); +}); + +test('hidden option takes priority over global config when includeHiddenElements is not defined', () => { + configure({ defaultHidden: true, defaultIncludeHiddenElements: true }); + render(); + expect(screen.queryByTestId('view', { hidden: false })).toBeFalsy(); +}); + +test('global config defaultIncludeElements option takes priority over defaultHidden when set at the same time', () => { + configure({ defaultHidden: false, defaultIncludeHiddenElements: true }); + render(); + expect(screen.getByTestId('view')).toBeTruthy(); +}); + +test('defaultHidden takes priority when it was set last', () => { + // also simulates the case when defaultIncludeHiddenElements is true by default in the config + configure({ defaultIncludeHiddenElements: true }); + configure({ defaultHidden: false }); + render(); + expect(screen.queryByTestId('view')).toBeFalsy(); +}); + +test('defaultIncludeHiddenElements takes priority when it was set last', () => { + // also simulates the case when defaultHidden is true by default in the config + configure({ defaultHidden: true }); + configure({ defaultIncludeHiddenElements: false }); + render(); + expect(screen.queryByTestId('view')).toBeFalsy(); +}); diff --git a/src/helpers/findAll.ts b/src/helpers/findAll.ts index a29357cf6..841ddf7c0 100644 --- a/src/helpers/findAll.ts +++ b/src/helpers/findAll.ts @@ -3,6 +3,8 @@ import { getConfig } from '../config'; import { isHiddenFromAccessibility } from './accessiblity'; interface FindAllOptions { + includeHiddenElements?: boolean; + /** RTL-compatible alias to `includeHiddenElements` */ hidden?: boolean; } @@ -13,8 +15,12 @@ export function findAll( ) { const results = root.findAll(predicate); - const hidden = options?.hidden ?? getConfig().defaultHidden; - if (hidden) { + const includeHiddenElements = + options?.includeHiddenElements ?? + options?.hidden ?? + getConfig()?.defaultIncludeHiddenElements; + + if (includeHiddenElements) { return results; } diff --git a/src/queries/__tests__/a11yState.test.tsx b/src/queries/__tests__/a11yState.test.tsx index 73dc1cc57..7c9f94f45 100644 --- a/src/queries/__tests__/a11yState.test.tsx +++ b/src/queries/__tests__/a11yState.test.tsx @@ -238,10 +238,16 @@ test('byA11yState queries support hidden option', () => { ); expect(getByA11yState({ expanded: false })).toBeTruthy(); - expect(getByA11yState({ expanded: false }, { hidden: true })).toBeTruthy(); + expect( + getByA11yState({ expanded: false }, { includeHiddenElements: true }) + ).toBeTruthy(); - expect(queryByA11yState({ expanded: false }, { hidden: false })).toBeFalsy(); + expect( + queryByA11yState({ expanded: false }, { includeHiddenElements: false }) + ).toBeFalsy(); expect(() => - getByA11yState({ expanded: false }, { hidden: false }) - ).toThrow(); + getByA11yState({ expanded: false }, { includeHiddenElements: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with expanded state: false"` + ); }); diff --git a/src/queries/__tests__/a11yValue.test.tsx b/src/queries/__tests__/a11yValue.test.tsx index c1b7c84b9..dc15c9383 100644 --- a/src/queries/__tests__/a11yValue.test.tsx +++ b/src/queries/__tests__/a11yValue.test.tsx @@ -101,8 +101,16 @@ test('byA11yValue queries support hidden option', () => { ); 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(); + expect( + getByA11yValue({ max: 10 }, { includeHiddenElements: true }) + ).toBeTruthy(); + + expect( + queryByA11yValue({ max: 10 }, { includeHiddenElements: false }) + ).toBeFalsy(); + expect(() => + getByA11yValue({ max: 10 }, { includeHiddenElements: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with accessibilityValue: {"max":10}"` + ); }); diff --git a/src/queries/__tests__/displayValue.test.tsx b/src/queries/__tests__/displayValue.test.tsx index ff2567ea0..1ba94bff2 100644 --- a/src/queries/__tests__/displayValue.test.tsx +++ b/src/queries/__tests__/displayValue.test.tsx @@ -106,8 +106,16 @@ test('byDisplayValue queries support hidden option', () => { ); expect(getByDisplayValue('hidden')).toBeTruthy(); - expect(getByDisplayValue('hidden', { hidden: true })).toBeTruthy(); - - expect(queryByDisplayValue('hidden', { hidden: false })).toBeFalsy(); - expect(() => getByDisplayValue('hidden', { hidden: false })).toThrow(); + expect( + getByDisplayValue('hidden', { includeHiddenElements: true }) + ).toBeTruthy(); + + expect( + queryByDisplayValue('hidden', { includeHiddenElements: false }) + ).toBeFalsy(); + expect(() => + getByDisplayValue('hidden', { includeHiddenElements: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with displayValue: hidden"` + ); }); diff --git a/src/queries/__tests__/hintText.test.tsx b/src/queries/__tests__/hintText.test.tsx index 02ad9c596..4adc5bb33 100644 --- a/src/queries/__tests__/hintText.test.tsx +++ b/src/queries/__tests__/hintText.test.tsx @@ -115,8 +115,14 @@ test('byHintText queries support hidden option', () => { ); expect(getByHintText('hidden')).toBeTruthy(); - expect(getByHintText('hidden', { hidden: true })).toBeTruthy(); - - expect(queryByHintText('hidden', { hidden: false })).toBeFalsy(); - expect(() => getByHintText('hidden', { hidden: false })).toThrow(); + expect(getByHintText('hidden', { includeHiddenElements: true })).toBeTruthy(); + + expect( + queryByHintText('hidden', { includeHiddenElements: false }) + ).toBeFalsy(); + expect(() => + getByHintText('hidden', { includeHiddenElements: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with accessibilityHint: hidden"` + ); }); diff --git a/src/queries/__tests__/labelText.test.tsx b/src/queries/__tests__/labelText.test.tsx index d1ce5a51b..9e1b5c0cd 100644 --- a/src/queries/__tests__/labelText.test.tsx +++ b/src/queries/__tests__/labelText.test.tsx @@ -152,8 +152,16 @@ test('byLabelText queries support hidden option', () => { ); expect(getByLabelText('hidden')).toBeTruthy(); - expect(getByLabelText('hidden', { hidden: true })).toBeTruthy(); - - expect(queryByLabelText('hidden', { hidden: false })).toBeFalsy(); - expect(() => getByLabelText('hidden', { hidden: false })).toThrow(); + expect( + getByLabelText('hidden', { includeHiddenElements: true }) + ).toBeTruthy(); + + expect( + queryByLabelText('hidden', { includeHiddenElements: false }) + ).toBeFalsy(); + expect(() => + getByLabelText('hidden', { includeHiddenElements: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with accessibilityLabel: hidden"` + ); }); diff --git a/src/queries/__tests__/placeholderText.test.tsx b/src/queries/__tests__/placeholderText.test.tsx index 973691eee..ef75bfe36 100644 --- a/src/queries/__tests__/placeholderText.test.tsx +++ b/src/queries/__tests__/placeholderText.test.tsx @@ -65,8 +65,16 @@ test('byPlaceholderText queries support hidden option', () => { ); expect(getByPlaceholderText('hidden')).toBeTruthy(); - expect(getByPlaceholderText('hidden', { hidden: true })).toBeTruthy(); + expect( + getByPlaceholderText('hidden', { includeHiddenElements: true }) + ).toBeTruthy(); - expect(queryByPlaceholderText('hidden', { hidden: false })).toBeFalsy(); - expect(() => getByPlaceholderText('hidden', { hidden: false })).toThrow(); + expect( + queryByPlaceholderText('hidden', { includeHiddenElements: false }) + ).toBeFalsy(); + expect(() => + getByPlaceholderText('hidden', { includeHiddenElements: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with placeholder: hidden"` + ); }); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index 25e5c9cc1..211b0637d 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -706,8 +706,12 @@ test('byRole queries support hidden option', () => { ); expect(getByRole('button')).toBeTruthy(); - expect(getByRole('button', { hidden: true })).toBeTruthy(); + expect(getByRole('button', { includeHiddenElements: true })).toBeTruthy(); - expect(queryByRole('button', { hidden: false })).toBeFalsy(); - expect(() => getByRole('button', { hidden: false })).toThrow(); + expect(queryByRole('button', { includeHiddenElements: false })).toBeFalsy(); + expect(() => + getByRole('button', { includeHiddenElements: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with role: "button""` + ); }); diff --git a/src/queries/__tests__/testId.test.tsx b/src/queries/__tests__/testId.test.tsx index f60ebe3b7..f064d411c 100644 --- a/src/queries/__tests__/testId.test.tsx +++ b/src/queries/__tests__/testId.test.tsx @@ -141,8 +141,12 @@ test('byTestId queries support hidden option', () => { ); expect(getByTestId('hidden')).toBeTruthy(); - expect(getByTestId('hidden', { hidden: true })).toBeTruthy(); + expect(getByTestId('hidden', { includeHiddenElements: true })).toBeTruthy(); - expect(queryByTestId('hidden', { hidden: false })).toBeFalsy(); - expect(() => getByTestId('hidden', { hidden: false })).toThrow(); + expect(queryByTestId('hidden', { includeHiddenElements: false })).toBeFalsy(); + expect(() => + getByTestId('hidden', { includeHiddenElements: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with testID: hidden"` + ); }); diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index 9195e22bb..9ae05cbb3 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -473,8 +473,12 @@ test('byText support hidden option', () => { ); expect(getByText(/hidden/i)).toBeTruthy(); - expect(getByText(/hidden/i, { hidden: true })).toBeTruthy(); + expect(getByText(/hidden/i, { includeHiddenElements: true })).toBeTruthy(); - expect(queryByText(/hidden/i, { hidden: false })).toBeFalsy(); - expect(() => getByText(/hidden/i, { hidden: false })).toThrow(); + expect(queryByText(/hidden/i, { includeHiddenElements: false })).toBeFalsy(); + expect(() => + getByText(/hidden/i, { includeHiddenElements: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with text: /hidden/i"` + ); }); diff --git a/src/queries/options.ts b/src/queries/options.ts index 8687b26e4..2468723a5 100644 --- a/src/queries/options.ts +++ b/src/queries/options.ts @@ -1,6 +1,12 @@ import { NormalizerFn } from '../matches'; -export type CommonQueryOptions = { hidden?: boolean }; +export type CommonQueryOptions = { + /** Should query include elements hidden from accessibility. */ + includeHiddenElements?: boolean; + + /** RTL-compatibile alias to `includeHiddenElements`. */ + hidden?: boolean; +}; export type TextMatchOptions = { exact?: boolean; diff --git a/typings/index.flow.js b/typings/index.flow.js index 7f30bad3d..1afeddc0f 100644 --- a/typings/index.flow.js +++ b/typings/index.flow.js @@ -8,7 +8,10 @@ type QueryAllReturn = Array | []; type FindReturn = Promise; type FindAllReturn = Promise; -type CommonQueryOptions = { hidden?: boolean }; +type CommonQueryOptions = { + includeHiddenElements?: boolean, + hidden?: boolean, +}; type TextMatch = string | RegExp; declare type NormalizerFn = (textToNormalize: string) => string; @@ -463,10 +466,18 @@ declare module '@testing-library/react-native' { declare interface Config { asyncUtilTimeout: number; + defaultIncludeHiddenElements: boolean; defaultDebugOptions?: $Shape; } - declare export var configure: (options: $Shape) => void; + declare interface ConfigAliasOptions { + /** Alias to `defaultIncludeHiddenElements` for RTL compatibility */ + defaultHidden: boolean; + } + + declare export var configure: ( + options: $Shape + ) => void; declare export var resetToDefaults: () => void; declare export var act: (callback: () => void) => Thenable; diff --git a/website/docs/API.md b/website/docs/API.md index e8b76b5a6..12e190c14 100644 --- a/website/docs/API.md +++ b/website/docs/API.md @@ -50,7 +50,7 @@ title: API - [Configuration](#configuration) - [`configure`](#configure) - [`asyncUtilTimeout` option](#asyncutiltimeout-option) - - [`defaultHidden` option](#defaulthidden-option) + - [`defaultIncludeHiddenElements` option](#defaultincludehiddenelements-option) - [`defaultDebugOptions` option](#defaultdebugoptions-option) - [`resetToDefaults()`](#resettodefaults) - [Environment variables](#environment-variables) @@ -791,9 +791,13 @@ function configure(options: Partial) {} Default timeout, in ms, for async helper functions (`waitFor`, `waitForElementToBeRemoved`) and `findBy*` queries. Defaults to 1000 ms. -#### `defaultHidden` option +#### `defaultIncludeHiddenElements` 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. +Default value for [includeHiddenElements](Queries.md#includehidden-option) query option for all queries. Defaults to `true`, which means that queries will match [elements hidden from accessibility](#ishiddenfromaccessibility) by default. + +Currently this option is set to `true` which means that queries will also match hidden elements. This is done to avoid breaking changes. However, we plan to change the default behavior to exclude hidden elements in the next major release. + +This option is also available as `defaultHidden` alias for compatibility with [React Testing Library](https://testing-library.com/docs/dom-testing-library/api-configuration/#defaulthidden). #### `defaultDebugOptions` option diff --git a/website/docs/Queries.md b/website/docs/Queries.md index 337cc08b4..7e8636424 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -27,7 +27,7 @@ title: Queries - [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) + - [`includeHiddenElements` option](#includehiddenelements-option) - [TextMatch](#textmatch) - [Examples](#examples) - [Precision](#precision) @@ -376,28 +376,28 @@ const element = screen.getByA11yValue({ min: 40 }); ## Common options -### `hidden` option +### `includeHiddenElements` 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. +All queries have the `includeHiddenElements` option which affects whether [elements hidden from accessibility](./API.md#ishiddenfromaccessibility) are matched by the query. You can configure the default value with the [`configure` function](API.md#configure). -An element is considered to be hidden from accessibility based on [`isHiddenFromAccessibility()`](./API.md#ishiddenfromaccessibility) function. +This option is also available as `hidden` alias for compatibility with [React Testing Library](https://testing-library.com/docs/queries/byrole#hidden). **Examples** ```tsx -render(I am hidden from accessibility); +render(Hidden from accessibility); -// Ignore hidden elements +// Exclude hidden elements expect( - screen.queryByText('I am hidden from accessibility', { hidden: false }) + screen.queryByText('Hidden from accessibility', { includeHiddenElements: false }) ).toBeFalsy(); -// Match hidden elements -expect(screen.getByText('I am hidden from accessibility')).toBeTruthy(); // Defaults to hidden: true for now +// Include hidden elements +expect(screen.getByText('Hidden from accessibility')).toBeTruthy(); expect( - screen.getByText('I am hidden from accessibility', { hidden: true }) + screen.getByText('Hidden from accessibility', { includeHiddenElements: true }) ).toBeTruthy(); ```