diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts
index 7419d3356..aed19ced6 100644
--- a/src/helpers/accessiblity.ts
+++ b/src/helpers/accessiblity.ts
@@ -4,7 +4,8 @@ import {
StyleSheet,
} from 'react-native';
import { ReactTestInstance } from 'react-test-renderer';
-import { getHostSiblings } from './component-tree';
+import { getTextContent } from './text-content';
+import { getHostSiblings, getUnsafeRootElement } from './component-tree';
import { getHostComponentNames } from './host-component-names';
type IsInaccessibleOptions = {
@@ -233,3 +234,23 @@ export function isElementSelected(
const { accessibilityState, 'aria-selected': ariaSelected } = element.props;
return ariaSelected ?? accessibilityState?.selected ?? false;
}
+
+export function getAccessibleName(
+ element: ReactTestInstance
+): string | undefined {
+ const label = getAccessibilityLabel(element);
+ if (label) {
+ return label;
+ }
+
+ const labelElementId = getAccessibilityLabelledBy(element);
+ if (labelElementId) {
+ const rootElement = getUnsafeRootElement(element);
+ const labelElement = rootElement?.findByProps({ nativeID: labelElementId });
+ if (labelElement) {
+ return getTextContent(labelElement);
+ }
+ }
+
+ return getTextContent(element);
+}
diff --git a/src/matchers/__tests__/to-have-accessible-name.test.tsx b/src/matchers/__tests__/to-have-accessible-name.test.tsx
new file mode 100644
index 000000000..1a355efc7
--- /dev/null
+++ b/src/matchers/__tests__/to-have-accessible-name.test.tsx
@@ -0,0 +1,134 @@
+import * as React from 'react';
+import { View, Text, TextInput } from 'react-native';
+import { render, screen } from '../..';
+import '../extend-expect';
+
+test('toHaveAccessibleName() handles view with "accessibilityLabel" prop', () => {
+ render();
+ const element = screen.getByTestId('view');
+ expect(element).toHaveAccessibleName('Test label');
+ expect(element).not.toHaveAccessibleName('Other label');
+});
+
+test('toHaveAccessibleName() handles view with "aria-label" prop', () => {
+ render();
+ const element = screen.getByTestId('view');
+ expect(element).toHaveAccessibleName('Test label');
+ expect(element).not.toHaveAccessibleName('Other label');
+});
+
+test('toHaveAccessibleName() handles view with "accessibilityLabelledBy" prop', async () => {
+ render(
+
+ External label
+
+
+ );
+
+ const element = screen.getByTestId('input');
+ expect(element).toHaveAccessibleName('External label');
+ expect(element).not.toHaveAccessibleName('Other label');
+});
+
+test('toHaveAccessibleName() handles nested "accessibilityLabelledBy"', async () => {
+ render(
+ <>
+
+ External label
+
+
+ >
+ );
+
+ const element = screen.getByTestId('input');
+ expect(element).toHaveAccessibleName('External label');
+ expect(element).not.toHaveAccessibleName('Other label');
+});
+
+test('toHaveAccessibleName() handles view with nested "accessibilityLabelledBy" with no text', async () => {
+ render(
+ <>
+
+
+
+
+ >
+ );
+
+ const element = screen.getByTestId('text-input');
+ expect(element).not.toHaveAccessibleName();
+});
+
+test('toHaveAccessibleName() handles view with "aria-labelledby" prop', async () => {
+ render(
+
+ External label
+
+
+ );
+
+ const element = screen.getByTestId('input');
+ expect(element).toHaveAccessibleName('External label');
+ expect(element).not.toHaveAccessibleName('Other label');
+});
+
+test('toHaveAccessibleName() handles view with implicit accessible name', () => {
+ render(Text);
+ const element = screen.getByTestId('view');
+ expect(element).toHaveAccessibleName('Text');
+ expect(element).not.toHaveAccessibleName('Other text');
+});
+
+test('toHaveAccessibleName() supports calling without expected name', () => {
+ render();
+ const element = screen.getByTestId('view');
+
+ expect(element).toHaveAccessibleName();
+ expect(() => expect(element).not.toHaveAccessibleName())
+ .toThrowErrorMatchingInlineSnapshot(`
+ "expect(element).not.toHaveAccessibleName()
+
+ Expected element not to have accessible name:
+ undefined
+ Received:
+ Test label"
+ `);
+});
+
+test('toHaveAccessibleName() handles a view without name when called without expected name', () => {
+ render();
+ const element = screen.getByTestId('view');
+
+ expect(element).not.toHaveAccessibleName();
+ expect(() => expect(element).toHaveAccessibleName())
+ .toThrowErrorMatchingInlineSnapshot(`
+ "expect(element).toHaveAccessibleName()
+
+ Expected element to have accessible name:
+ undefined
+ Received:
+ "
+ `);
+});
+
+it('toHaveAccessibleName() rejects non-host element', () => {
+ const nonElement = 'This is not a ReactTestInstance';
+
+ expect(() => expect(nonElement).toHaveAccessibleName())
+ .toThrowErrorMatchingInlineSnapshot(`
+ "expect(received).toHaveAccessibleName()
+
+ received value must be a host element.
+ Received has type: string
+ Received has value: "This is not a ReactTestInstance""
+ `);
+
+ expect(() => expect(nonElement).not.toHaveAccessibleName())
+ .toThrowErrorMatchingInlineSnapshot(`
+ "expect(received).not.toHaveAccessibleName()
+
+ received value must be a host element.
+ Received has type: string
+ Received has value: "This is not a ReactTestInstance""
+ `);
+});
diff --git a/src/matchers/extend-expect.d.ts b/src/matchers/extend-expect.d.ts
index 55cb2df53..7d76db0c1 100644
--- a/src/matchers/extend-expect.d.ts
+++ b/src/matchers/extend-expect.d.ts
@@ -18,6 +18,7 @@ export interface JestNativeMatchers {
toBeVisible(): R;
toContainElement(element: ReactTestInstance | null): R;
toHaveAccessibilityValue(expectedValue: AccessibilityValueMatcher): R;
+ toHaveAccessibleName(expectedName?: TextMatch, options?: TextMatchOptions): R;
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
toHaveProp(name: string, expectedValue?: unknown): R;
toHaveStyle(style: StyleProp