diff --git a/.eslintrc b/.eslintrc
index d40d133a2..33ed457bd 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -10,9 +10,10 @@
2,
{ "ignore": ["^@theme", "^@docusaurus", "^@generated"] }
],
- "react-native-a11y/has-valid-accessibility-ignores-invert-colors": 0,
"react-native/no-color-literals": "off",
+ "react-native/no-inline-styles": "off",
"react-native-a11y/has-valid-accessibility-descriptors": "off",
+ "react-native-a11y/has-valid-accessibility-ignores-invert-colors": 0,
"react-native-a11y/has-valid-accessibility-value": "off"
}
}
diff --git a/src/fireEvent.ts b/src/fireEvent.ts
index 2da697383..3cbf18a8c 100644
--- a/src/fireEvent.ts
+++ b/src/fireEvent.ts
@@ -1,13 +1,10 @@
import { ReactTestInstance } from 'react-test-renderer';
import act from './act';
+import { isHostElement } from './helpers/component-tree';
import { filterNodeByType } from './helpers/filterNodeByType';
type EventHandler = (...args: any) => unknown;
-const isHostElement = (element?: ReactTestInstance) => {
- return typeof element?.type === 'string';
-};
-
const isTextInput = (element?: ReactTestInstance) => {
if (!element) {
return false;
diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx
new file mode 100644
index 000000000..bd54403d1
--- /dev/null
+++ b/src/helpers/__tests__/accessiblity.test.tsx
@@ -0,0 +1,165 @@
+import React from 'react';
+import { View, Text, TextInput } from 'react-native';
+import { render, isInaccessible } from '../..';
+
+test('returns false for accessible elements', () => {
+ expect(
+ isInaccessible(render().getByTestId('subject'))
+ ).toBe(false);
+
+ expect(
+ isInaccessible(
+ render(Hello).getByTestId('subject')
+ )
+ ).toBe(false);
+
+ expect(
+ isInaccessible(
+ render().getByTestId('subject')
+ )
+ ).toBe(false);
+});
+
+test('detects elements with accessibilityElementsHidden prop', () => {
+ const view = render();
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('detects nested elements with accessibilityElementsHidden prop', () => {
+ const view = render(
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('detects deeply nested elements with accessibilityElementsHidden prop', () => {
+ const view = render(
+
+
+
+
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('detects elements with importantForAccessibility="no-hide-descendants" prop', () => {
+ const view = render(
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('detects nested elements with importantForAccessibility="no-hide-descendants" prop', () => {
+ const view = render(
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('detects elements with display=none', () => {
+ const view = render();
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('detects nested elements with display=none', () => {
+ const view = render(
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('detects deeply nested elements with display=none', () => {
+ const view = render(
+
+
+
+
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('detects elements with display=none with complex style', () => {
+ const view = render(
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('is not trigged by opacity = 0', () => {
+ const view = render();
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(false);
+});
+
+test('detects siblings of element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('detects deeply nested siblings of element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
+
+
+
+
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
+});
+
+test('is not triggered for element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(false);
+});
+
+test('is not triggered for child of element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
+
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(false);
+});
+
+test('is not triggered for descendent of element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
+
+
+
+
+
+
+
+
+ );
+ expect(isInaccessible(view.getByTestId('subject'))).toBe(false);
+});
diff --git a/src/helpers/__tests__/component-tree.test.tsx b/src/helpers/__tests__/component-tree.test.tsx
new file mode 100644
index 000000000..2b1b5c619
--- /dev/null
+++ b/src/helpers/__tests__/component-tree.test.tsx
@@ -0,0 +1,202 @@
+import React from 'react';
+import { View, Text, TextInput } from 'react-native';
+import { render } from '../..';
+import {
+ getHostChildren,
+ getHostParent,
+ getHostSelves,
+ getHostSiblings,
+} from '../component-tree';
+
+function MultipleHostChildren() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+test('returns host parent for host component', () => {
+ const view = render(
+
+
+
+
+
+
+ );
+
+ const hostParent = getHostParent(view.getByTestId('subject'));
+ expect(hostParent).toBe(view.getByTestId('parent'));
+
+ const hostGrandparent = getHostParent(hostParent);
+ expect(hostGrandparent).toBe(view.getByTestId('grandparent'));
+
+ expect(getHostParent(hostGrandparent)).toBe(null);
+});
+
+test('returns host parent for composite component', () => {
+ const view = render(
+
+
+
+
+ );
+
+ const compositeComponent = view.UNSAFE_getByType(MultipleHostChildren);
+ const hostParent = getHostParent(compositeComponent);
+ expect(hostParent).toBe(view.getByTestId('parent'));
+});
+
+test('returns host children for host component', () => {
+ const view = render(
+
+
+
+
+
+
+ );
+
+ const hostSubject = view.getByTestId('subject');
+ expect(getHostChildren(hostSubject)).toEqual([]);
+
+ const hostSibling = view.getByTestId('sibling');
+ const hostParent = view.getByTestId('parent');
+ expect(getHostChildren(hostParent)).toEqual([hostSubject, hostSibling]);
+
+ const hostGrandparent = view.getByTestId('grandparent');
+ expect(getHostChildren(hostGrandparent)).toEqual([hostParent]);
+});
+
+test('returns host children for composite component', () => {
+ const view = render(
+
+
+
+
+
+ );
+
+ expect(getHostChildren(view.getByTestId('parent'))).toEqual([
+ view.getByTestId('child1'),
+ view.getByTestId('child2'),
+ view.getByTestId('child3'),
+ view.getByTestId('subject'),
+ view.getByTestId('sibling'),
+ ]);
+});
+
+test('returns host selves for host components', () => {
+ const view = render(
+
+
+
+
+
+
+ );
+
+ const hostSubject = view.getByTestId('subject');
+ expect(getHostSelves(hostSubject)).toEqual([hostSubject]);
+
+ const hostSibling = view.getByTestId('sibling');
+ expect(getHostSelves(hostSibling)).toEqual([hostSibling]);
+
+ const hostParent = view.getByTestId('parent');
+ expect(getHostSelves(hostParent)).toEqual([hostParent]);
+
+ const hostGrandparent = view.getByTestId('grandparent');
+ expect(getHostSelves(hostGrandparent)).toEqual([hostGrandparent]);
+});
+
+test('returns host selves for React Native composite components', () => {
+ const view = render(
+
+ Text
+
+
+ );
+
+ const compositeText = view.getByText('Text');
+ const hostText = view.getByTestId('text');
+ expect(getHostSelves(compositeText)).toEqual([hostText]);
+
+ const compositeTextInputByValue = view.getByDisplayValue('TextInputValue');
+ const compositeTextInputByPlaceholder = view.getByPlaceholderText(
+ 'TextInputPlaceholder'
+ );
+ const hostTextInput = view.getByTestId('textInput');
+ expect(getHostSelves(compositeTextInputByValue)).toEqual([hostTextInput]);
+ expect(getHostSelves(compositeTextInputByPlaceholder)).toEqual([
+ hostTextInput,
+ ]);
+});
+
+test('returns host selves for custom composite components', () => {
+ const view = render(
+
+
+
+
+ );
+
+ const compositeComponent = view.UNSAFE_getByType(MultipleHostChildren);
+ const hostChild1 = view.getByTestId('child1');
+ const hostChild2 = view.getByTestId('child2');
+ const hostChild3 = view.getByTestId('child3');
+ expect(getHostSelves(compositeComponent)).toEqual([
+ hostChild1,
+ hostChild2,
+ hostChild3,
+ ]);
+});
+
+test('returns host siblings for host component', () => {
+ const view = render(
+
+
+
+
+
+
+
+
+ );
+
+ const hostSiblings = getHostSiblings(view.getByTestId('subject'));
+ expect(hostSiblings).toEqual([
+ view.getByTestId('siblingBefore'),
+ view.getByTestId('siblingAfter'),
+ view.getByTestId('child1'),
+ view.getByTestId('child2'),
+ view.getByTestId('child3'),
+ ]);
+});
+
+test('returns host siblings for composite component', () => {
+ const view = render(
+
+
+
+
+
+
+
+
+ );
+
+ const compositeComponent = view.UNSAFE_getByType(MultipleHostChildren);
+ const hostSiblings = getHostSiblings(compositeComponent);
+ expect(hostSiblings).toEqual([
+ view.getByTestId('siblingBefore'),
+ view.getByTestId('subject'),
+ view.getByTestId('siblingAfter'),
+ ]);
+});
diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts
new file mode 100644
index 000000000..a4dc1885f
--- /dev/null
+++ b/src/helpers/accessiblity.ts
@@ -0,0 +1,51 @@
+import { StyleSheet } from 'react-native';
+import { ReactTestInstance } from 'react-test-renderer';
+import { getHostSiblings } from './component-tree';
+
+export function isInaccessible(element: ReactTestInstance | null): boolean {
+ if (element == null) {
+ return true;
+ }
+
+ let current: ReactTestInstance | null = element;
+ while (current) {
+ if (isSubtreeInaccessible(current)) {
+ return true;
+ }
+
+ current = current.parent;
+ }
+
+ return false;
+}
+
+function isSubtreeInaccessible(element: ReactTestInstance | null): boolean {
+ if (element == null) {
+ return true;
+ }
+
+ // iOS: accessibilityElementsHidden
+ // See: https://reactnative.dev/docs/accessibility#accessibilityelementshidden-ios
+ if (element.props.accessibilityElementsHidden) {
+ return true;
+ }
+
+ // Android: importantForAccessibility
+ // See: https://reactnative.dev/docs/accessibility#importantforaccessibility-android
+ if (element.props.importantForAccessibility === 'no-hide-descendants') {
+ return true;
+ }
+
+ // Note that `opacity: 0` is not threated as inassessible on iOS
+ const flatStyle = StyleSheet.flatten(element.props.style) ?? {};
+ if (flatStyle.display === 'none') return true;
+
+ // iOS: accessibilityViewIsModal
+ // See: https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios
+ const hostSiblings = getHostSiblings(element);
+ if (hostSiblings.some((sibling) => sibling.props.accessibilityViewIsModal)) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts
new file mode 100644
index 000000000..340472d72
--- /dev/null
+++ b/src/helpers/component-tree.ts
@@ -0,0 +1,89 @@
+import { ReactTestInstance } from 'react-test-renderer';
+
+/**
+ * Checks if the given element is a host element.
+ * @param element The element to check.
+ */
+export function isHostElement(element?: ReactTestInstance | null): boolean {
+ return typeof element?.type === 'string';
+}
+
+/**
+ * Returns first host ancestor for given element.
+ * @param element The element start traversing from.
+ */
+export function getHostParent(
+ element: ReactTestInstance | null
+): ReactTestInstance | null {
+ if (element == null) {
+ return null;
+ }
+
+ let current = element.parent;
+ while (current) {
+ if (isHostElement(current)) {
+ return current;
+ }
+
+ current = current.parent;
+ }
+
+ return null;
+}
+
+/**
+ * Returns host children for given element.
+ * @param element The element start traversing from.
+ */
+export function getHostChildren(
+ element: ReactTestInstance | null
+): ReactTestInstance[] {
+ if (element == null) {
+ return [];
+ }
+
+ const hostChildren: ReactTestInstance[] = [];
+
+ element.children.forEach((child) => {
+ if (typeof child !== 'object') {
+ return;
+ }
+
+ if (isHostElement(child)) {
+ hostChildren.push(child);
+ } else {
+ hostChildren.push(...getHostChildren(child));
+ }
+ });
+
+ return hostChildren;
+}
+
+/**
+ * Return the array of host elements that represent the passed element.
+ *
+ * @param element The element start traversing from.
+ * @returns If the passed element is a host element, it will return an array containing only that element,
+ * if the passed element is a composite element, it will return an array containing its host children (zero, one or many).
+ */
+export function getHostSelves(
+ element: ReactTestInstance | null
+): ReactTestInstance[] {
+ return typeof element?.type === 'string'
+ ? [element]
+ : getHostChildren(element);
+}
+
+/**
+ * Returns host siblings for given element.
+ * @param element The element start traversing from.
+ */
+export function getHostSiblings(
+ element: ReactTestInstance | null
+): ReactTestInstance[] {
+ const hostParent = getHostParent(element);
+ const hostSelves = getHostSelves(element);
+ return getHostChildren(hostParent).filter(
+ (sibling) => !hostSelves.includes(sibling)
+ );
+}
diff --git a/src/pure.ts b/src/pure.ts
index c1ad3e00d..c3f448249 100644
--- a/src/pure.ts
+++ b/src/pure.ts
@@ -8,6 +8,7 @@ import { within, getQueriesForElement } from './within';
import { getDefaultNormalizer } from './matches';
import { renderHook } from './renderHook';
import { screen } from './screen';
+import { isInaccessible } from './helpers/accessiblity';
export type {
RenderOptions,
@@ -26,3 +27,4 @@ export { within, getQueriesForElement };
export { getDefaultNormalizer };
export { renderHook };
export { screen };
+export { isInaccessible };
diff --git a/website/docs/API.md b/website/docs/API.md
index f297591fe..78ce0af3d 100644
--- a/website/docs/API.md
+++ b/website/docs/API.md
@@ -41,6 +41,8 @@ title: API
- [Examples](#examples)
- [With `initialProps`](#with-initialprops)
- [With `wrapper`](#with-wrapper)
+- [Accessibility](#accessibility)
+ - [`isInaccessible`](#isinaccessible)
This page gathers public API of React Native Testing Library along with usage examples.
@@ -225,7 +227,11 @@ Failing to call `cleanup` when you've called `render` could result in a memory l
## `fireEvent`
```ts
-fireEvent(element: ReactTestInstance, eventName: string, ...data: Array): void
+function fireEvent(
+ element: ReactTestInstance,
+ eventName: string,
+ ...data: Array
+): void {}
```
Fires native-like event with data.
@@ -485,8 +491,13 @@ If you receive warnings related to `act()` function consult our [Undestanding Ac
Defined as:
```jsx
-function within(instance: ReactTestInstance): Queries
-function getQueriesForElement(instance: ReactTestInstance): Queries
+function within(
+ element: ReactTestInstance
+): Queries {}
+
+function getQueriesForElement(
+ element: ReactTestInstance
+): Queries {}
```
`within` (also available as `getQueriesForElement` alias) performs [queries](./Queries.md) scoped to given element.
@@ -669,3 +680,29 @@ it('should use context value', () => {
// ...
});
```
+
+## Accessibility
+
+### `isInaccessible`
+
+```ts
+function isInaccessible(
+ element: ReactTestInstance | null
+): boolean {}
+```
+
+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.
+
+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`
+
+Specifying `accessible={false}`, `accessiblityRole="none"`, or `importantForAccessibility="no"` props does not cause the element to become inaccessible.