From ff752e521273987e8420870920105bf11e751e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 26 May 2026 13:37:39 +0200 Subject: [PATCH 1/6] tests --- src/helpers/__tests__/accessibility.test.tsx | 24 +++++++++++++++++ src/queries/__tests__/label-text.test.tsx | 28 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/helpers/__tests__/accessibility.test.tsx b/src/helpers/__tests__/accessibility.test.tsx index 39edcdd17..324c04145 100644 --- a/src/helpers/__tests__/accessibility.test.tsx +++ b/src/helpers/__tests__/accessibility.test.tsx @@ -463,6 +463,30 @@ describe('computeAriaLabel', () => { 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('concatenates labels referenced by comma-separated aria-labelledby', async () => { + await render( + + + First + Second + , + ); + + expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('First Second'); + }); + test('supports Image with alt prop', async () => { await render(Image Alt); expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('Image Alt'); diff --git a/src/queries/__tests__/label-text.test.tsx b/src/queries/__tests__/label-text.test.tsx index bc86fbe46..bf274ed43 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( <> @@ -208,6 +222,20 @@ test('getByLabelText supports aria-labelledby', async () => { expect(screen.getByLabelText(/text label/i)).toBe(screen.getByTestId('text-input')); }); +test('getByLabelText matches concatenated comma-separated aria-labelledby labels', async () => { + await render( + <> + First + Second + + , + ); + + expect(screen.getByLabelText('First Second')).toBe(screen.getByTestId('text-input')); + expect(screen.queryByLabelText('First')).toBeNull(); + expect(screen.queryByLabelText('Second')).toBeNull(); +}); + test('getByLabelText supports nested aria-labelledby', async () => { await render( <> From 957142e3c9186fddc85ca979fa3678d0c3bef273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 26 May 2026 13:40:38 +0200 Subject: [PATCH 2/6] impl --- src/helpers/accessibility.ts | 44 ++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index de8fab7bf..d491bf462 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(' '); } } @@ -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.split(/\s*,\s*/g).filter(Boolean); + } + + 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; From d0093786c411fb9db3179722e583c07e3c7c65f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 26 May 2026 13:42:05 +0200 Subject: [PATCH 3/6] docs --- skills/react-native-testing/references/api-reference-v14.md | 1 + website/docs/14.x/docs/api/jest-matchers.mdx | 2 ++ website/docs/14.x/docs/api/queries.mdx | 2 ++ 3 files changed, 5 insertions(+) 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/website/docs/14.x/docs/api/jest-matchers.mdx b/website/docs/14.x/docs/api/jest-matchers.mdx index 89281d86b..d46fb2cd6 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 `aria-labelledby` or `accessibilityLabelledBy` references multiple elements, their text content is joined with spaces in the referenced order and matched as a single accessible name. + 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..d92c414d4 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 `aria-labelledby` or `accessibilityLabelledBy` references multiple elements, their text content is joined with spaces in the referenced order and matched as a single label. + ```jsx import { render, screen } from '@testing-library/react-native'; From ee5013190eaa2744ee14b5f8a17af089a5554b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 26 May 2026 13:53:02 +0200 Subject: [PATCH 4/6] . --- src/helpers/__tests__/accessibility.test.tsx | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/helpers/__tests__/accessibility.test.tsx b/src/helpers/__tests__/accessibility.test.tsx index 324c04145..34ee407f3 100644 --- a/src/helpers/__tests__/accessibility.test.tsx +++ b/src/helpers/__tests__/accessibility.test.tsx @@ -433,6 +433,18 @@ describe('computeAriaLabel', () => { expect(computeAriaLabel(screen.getByTestId('text-content'))).toBeUndefined(); }); + test('concatenates labels referenced by comma-separated aria-labelledby', async () => { + await render( + + + First + Second + , + ); + + expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('First Second'); + }); + test('label priority', async () => { await render( @@ -463,22 +475,21 @@ describe('computeAriaLabel', () => { expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External'); }); - test('concatenates labels referenced by accessibilityLabelledBy array', async () => { + test('supports accessibilityLabelledBy array with a single item', async () => { await render( - - First - Second + + External , ); - expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('First Second'); + expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External'); }); - test('concatenates labels referenced by comma-separated aria-labelledby', async () => { + test('concatenates labels referenced by accessibilityLabelledBy array', async () => { await render( - + First Second , From 42482c7863d3e8bb6f91380afb935f828ea256ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 26 May 2026 13:58:07 +0200 Subject: [PATCH 5/6] code review changes --- src/helpers/__tests__/accessibility.test.tsx | 9 ++++----- src/helpers/accessibility.ts | 4 ++-- src/queries/__tests__/label-text.test.tsx | 14 -------------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/helpers/__tests__/accessibility.test.tsx b/src/helpers/__tests__/accessibility.test.tsx index 34ee407f3..299fb9603 100644 --- a/src/helpers/__tests__/accessibility.test.tsx +++ b/src/helpers/__tests__/accessibility.test.tsx @@ -433,16 +433,15 @@ describe('computeAriaLabel', () => { expect(computeAriaLabel(screen.getByTestId('text-content'))).toBeUndefined(); }); - test('concatenates labels referenced by comma-separated aria-labelledby', async () => { + test('does not fall back to aria-label when aria-labelledby resolves to empty text', async () => { await render( - - First - Second + + , ); - expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('First Second'); + expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual(''); }); test('label priority', async () => { diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index d491bf462..c29a59d70 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -166,7 +166,7 @@ export function computeAriaLabel(instance: TestInstance): string | undefined { .filter((labelText): labelText is string => labelText !== undefined); if (labelTexts.length > 0) { - return labelTexts.join(' '); + return labelTexts.join(' ').trim().replace(/\s+/g, ' '); } } @@ -186,7 +186,7 @@ export function computeAriaLabel(instance: TestInstance): string | undefined { function getAriaLabelledByIds(instance: TestInstance): string[] { const ariaLabelledBy = instance.props['aria-labelledby']; if (typeof ariaLabelledBy === 'string') { - return ariaLabelledBy.split(/\s*,\s*/g).filter(Boolean); + return [ariaLabelledBy]; } const accessibilityLabelledBy = instance.props.accessibilityLabelledBy; diff --git a/src/queries/__tests__/label-text.test.tsx b/src/queries/__tests__/label-text.test.tsx index bf274ed43..07b1e6569 100644 --- a/src/queries/__tests__/label-text.test.tsx +++ b/src/queries/__tests__/label-text.test.tsx @@ -222,20 +222,6 @@ test('getByLabelText supports aria-labelledby', async () => { expect(screen.getByLabelText(/text label/i)).toBe(screen.getByTestId('text-input')); }); -test('getByLabelText matches concatenated comma-separated aria-labelledby labels', async () => { - await render( - <> - First - Second - - , - ); - - expect(screen.getByLabelText('First Second')).toBe(screen.getByTestId('text-input')); - expect(screen.queryByLabelText('First')).toBeNull(); - expect(screen.queryByLabelText('Second')).toBeNull(); -}); - test('getByLabelText supports nested aria-labelledby', async () => { await render( <> From c950b6713484b0d959fc2a9dafd9fd1304e8e3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 26 May 2026 14:00:54 +0200 Subject: [PATCH 6/6] docs --- website/docs/14.x/docs/api/jest-matchers.mdx | 2 +- website/docs/14.x/docs/api/queries.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/14.x/docs/api/jest-matchers.mdx b/website/docs/14.x/docs/api/jest-matchers.mdx index d46fb2cd6..54c9465de 100644 --- a/website/docs/14.x/docs/api/jest-matchers.mdx +++ b/website/docs/14.x/docs/api/jest-matchers.mdx @@ -183,7 +183,7 @@ 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 `aria-labelledby` or `accessibilityLabelledBy` references multiple elements, their text content is joined with spaces in the referenced order and matched as a single accessible name. +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. diff --git a/website/docs/14.x/docs/api/queries.mdx b/website/docs/14.x/docs/api/queries.mdx index d92c414d4..3701749b2 100644 --- a/website/docs/14.x/docs/api/queries.mdx +++ b/website/docs/14.x/docs/api/queries.mdx @@ -239,7 +239,7 @@ 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 `aria-labelledby` or `accessibilityLabelledBy` references multiple elements, their text content is joined with spaces in the referenced order and matched as a single label. +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';