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(Image Alt); 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';