diff --git a/skills/react-native-testing/references/api-reference-v14.md b/skills/react-native-testing/references/api-reference-v14.md
index e796f6466..f5a2d1838 100644
--- a/skills/react-native-testing/references/api-reference-v14.md
+++ b/skills/react-native-testing/references/api-reference-v14.md
@@ -181,6 +181,7 @@ getByLabelText(text: TextMatch, options?: { exact?: boolean; normalizer?: Functi
```
Matches by `aria-label`/`accessibilityLabel` or text content of element referenced by `aria-labelledby`/`accessibilityLabelledBy`.
+When multiple elements are referenced, their text content is joined with spaces in the referenced order and matched as a single label.
#### `*ByPlaceholderText`
diff --git a/src/helpers/__tests__/accessibility.test.tsx b/src/helpers/__tests__/accessibility.test.tsx
index 39edcdd17..299fb9603 100644
--- a/src/helpers/__tests__/accessibility.test.tsx
+++ b/src/helpers/__tests__/accessibility.test.tsx
@@ -433,6 +433,17 @@ describe('computeAriaLabel', () => {
expect(computeAriaLabel(screen.getByTestId('text-content'))).toBeUndefined();
});
+ test('does not fall back to aria-label when aria-labelledby resolves to empty text', async () => {
+ await render(
+
+
+
+ ,
+ );
+
+ expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('');
+ });
+
test('label priority', async () => {
await render(
@@ -463,6 +474,29 @@ describe('computeAriaLabel', () => {
expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External');
});
+ test('supports accessibilityLabelledBy array with a single item', async () => {
+ await render(
+
+
+ External
+ ,
+ );
+
+ expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External');
+ });
+
+ test('concatenates labels referenced by accessibilityLabelledBy array', async () => {
+ await render(
+
+
+ First
+ Second
+ ,
+ );
+
+ expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('First Second');
+ });
+
test('supports Image with alt prop', async () => {
await render();
expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('Image Alt');
diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts
index de8fab7bf..c29a59d70 100644
--- a/src/helpers/accessibility.ts
+++ b/src/helpers/accessibility.ts
@@ -150,17 +150,23 @@ export function computeAriaModal(instance: TestInstance): boolean | undefined {
}
export function computeAriaLabel(instance: TestInstance): string | undefined {
- const labelElementId =
- instance.props['aria-labelledby'] ?? instance.props.accessibilityLabelledBy;
- if (labelElementId) {
+ const labelElementIds = getAriaLabelledByIds(instance);
+ if (labelElementIds.length > 0) {
const container = getContainerInstance(instance);
- const labelInstance = findAll(
- container,
- (node) => isTestInstance(node) && node.props.nativeID === labelElementId,
- { includeHiddenElements: true },
- );
- if (labelInstance.length > 0) {
- return getTextContent(labelInstance[0]);
+ const labelTexts = labelElementIds
+ .map((labelElementId) => {
+ const labelInstance = findAll(
+ container,
+ (node) => isTestInstance(node) && node.props.nativeID === labelElementId,
+ { includeHiddenElements: true },
+ );
+
+ return labelInstance.length > 0 ? getTextContent(labelInstance[0]) : undefined;
+ })
+ .filter((labelText): labelText is string => labelText !== undefined);
+
+ if (labelTexts.length > 0) {
+ return labelTexts.join(' ').trim().replace(/\s+/g, ' ');
}
}
@@ -177,6 +183,24 @@ export function computeAriaLabel(instance: TestInstance): string | undefined {
return undefined;
}
+function getAriaLabelledByIds(instance: TestInstance): string[] {
+ const ariaLabelledBy = instance.props['aria-labelledby'];
+ if (typeof ariaLabelledBy === 'string') {
+ return [ariaLabelledBy];
+ }
+
+ const accessibilityLabelledBy = instance.props.accessibilityLabelledBy;
+ if (Array.isArray(accessibilityLabelledBy)) {
+ return accessibilityLabelledBy;
+ }
+
+ if (typeof accessibilityLabelledBy === 'string') {
+ return [accessibilityLabelledBy];
+ }
+
+ return [];
+}
+
// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#busy-state
export function computeAriaBusy({ props }: TestInstance): boolean {
return props['aria-busy'] ?? props.accessibilityState?.busy ?? false;
diff --git a/src/queries/__tests__/label-text.test.tsx b/src/queries/__tests__/label-text.test.tsx
index bc86fbe46..07b1e6569 100644
--- a/src/queries/__tests__/label-text.test.tsx
+++ b/src/queries/__tests__/label-text.test.tsx
@@ -182,6 +182,20 @@ test('getByLabelText supports accessibilityLabelledBy', async () => {
expect(screen.getByLabelText(/input/)).toBe(screen.getByTestId('textInput'));
});
+test('getByLabelText matches concatenated accessibilityLabelledBy array labels', async () => {
+ await render(
+ <>
+ First
+ Second
+
+ >,
+ );
+
+ expect(screen.getByLabelText('First Second')).toBe(screen.getByTestId('textInput'));
+ expect(screen.queryByLabelText('First')).toBeNull();
+ expect(screen.queryByLabelText('Second')).toBeNull();
+});
+
test('getByLabelText supports nested accessibilityLabelledBy', async () => {
await render(
<>
diff --git a/website/docs/14.x/docs/api/jest-matchers.mdx b/website/docs/14.x/docs/api/jest-matchers.mdx
index 89281d86b..54c9465de 100644
--- a/website/docs/14.x/docs/api/jest-matchers.mdx
+++ b/website/docs/14.x/docs/api/jest-matchers.mdx
@@ -183,6 +183,8 @@ Checks if an element has the specified accessible name. Accepts `string` or `Reg
The accessible name comes from `aria-labelledby`, `accessibilityLabelledBy`, `aria-label`, and `accessibilityLabel` props. For `Image` elements, the `alt` prop is also used. If none are present, the element's text content is used.
+When `accessibilityLabelledBy` references multiple elements with an array, their text content is joined with spaces in the referenced order and matched as a single accessible name. `aria-labelledby` follows React Native's single `nativeID` value behavior.
+
Without a `name` parameter (or with `undefined`), it only checks whether the element has any accessible name.
### `toHaveProp()`
diff --git a/website/docs/14.x/docs/api/queries.mdx b/website/docs/14.x/docs/api/queries.mdx
index 376ba7907..3701749b2 100644
--- a/website/docs/14.x/docs/api/queries.mdx
+++ b/website/docs/14.x/docs/api/queries.mdx
@@ -239,6 +239,8 @@ Returns a `TestInstance` with matching label:
- or by matching text content of view referenced by [`aria-labelledby`](https://reactnative.dev/docs/accessibility#aria-labelledby-android)/[`accessibilityLabelledBy`](https://reactnative.dev/docs/accessibility#accessibilitylabelledby-android) prop
- or by matching the [`alt`](https://reactnative.dev/docs/image#alt) prop on `Image` elements
+When `accessibilityLabelledBy` references multiple elements with an array, their text content is joined with spaces in the referenced order and matched as a single label. `aria-labelledby` follows React Native's single `nativeID` value behavior.
+
```jsx
import { render, screen } from '@testing-library/react-native';