Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support aria-label and aria-labelledby props #1475

Merged
merged 3 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ describe('mapPropsForQueryError', () => {
accessibilityLabelledBy: 'LABELLED_BY',
accessibilityRole: 'ROLE',
accessibilityHint: 'HINT',
'aria-label': 'ARIA_LABEL',
'aria-labelledby': 'ARIA_LABELLED_BY',
placeholder: 'PLACEHOLDER',
value: 'VALUE',
defaultValue: 'DEFAULT_VALUE',
Expand Down
14 changes: 14 additions & 0 deletions src/helpers/accessiblity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,17 @@ export function isAccessibilityElement(
export function getAccessibilityRole(element: ReactTestInstance) {
return element.props.role ?? element.props.accessibilityRole;
}

export function getAccessibilityLabel(
element: ReactTestInstance
): string | undefined {
return element.props['aria-label'] ?? element.props.accessibilityLabel;
}

export function getAccessibilityLabelledBy(
element: ReactTestInstance
): string | undefined {
return (
element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy
);
}
20 changes: 11 additions & 9 deletions src/helpers/format-default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ import { StyleSheet, ViewStyle } from 'react-native';
import { MapPropsFunction } from './format';

const propsToDisplay = [
'testID',
'nativeID',
'accessibilityElementsHidden',
'accessibilityViewIsModal',
'importantForAccessibility',
'accessibilityRole',
'accessibilityHint',
'accessibilityLabel',
'accessibilityLabelledBy',
'accessibilityHint',
'role',
'accessibilityRole',
'accessibilityViewIsModal',
'aria-hidden',
'placeholder',
'value',
'aria-label',
'aria-labelledby',
'defaultValue',
'importantForAccessibility',
'nativeID',
'placeholder',
'role',
'testID',
'title',
'value',
];

/**
Expand Down
22 changes: 15 additions & 7 deletions src/helpers/matchers/matchLabelText.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
import { ReactTestInstance } from 'react-test-renderer';
import { matches, TextMatch, TextMatchOptions } from '../../matches';
import {
getAccessibilityLabel,
getAccessibilityLabelledBy,
} from '../accessiblity';
import { findAll } from '../findAll';
import { matchTextContent } from './matchTextContent';

export function matchLabelText(
root: ReactTestInstance,
element: ReactTestInstance,
text: TextMatch,
expectedText: TextMatch,
options: TextMatchOptions = {}
) {
return (
matchAccessibilityLabel(element, text, options) ||
matchAccessibilityLabel(element, expectedText, options) ||
matchAccessibilityLabelledBy(
root,
element.props.accessibilityLabelledBy,
text,
getAccessibilityLabelledBy(element),
expectedText,
options
)
);
}

function matchAccessibilityLabel(
element: ReactTestInstance,
text: TextMatch,
extpectedLabel: TextMatch,
options: TextMatchOptions
) {
const { exact, normalizer } = options;
return matches(text, element.props.accessibilityLabel, normalizer, exact);
return matches(
extpectedLabel,
getAccessibilityLabel(element),
options.normalizer,
options.exact
);
}

function matchAccessibilityLabelledBy(
Expand Down
2 changes: 1 addition & 1 deletion src/matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type TextMatchOptions = {

export function matches(
matcher: TextMatch,
text: string,
text: string | undefined,
normalizer: NormalizerFn = getDefaultNormalizer(),
exact: boolean = true
): boolean {
Expand Down
66 changes: 59 additions & 7 deletions src/queries/__tests__/labelText.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ const TEXT_HINT = 'static text';
const NO_MATCHES_TEXT: any = 'not-existent-element';

const getMultipleInstancesFoundMessage = (value: string) => {
return `Found multiple elements with accessibilityLabel: ${value}`;
return `Found multiple elements with accessibility label: ${value}`;
};

const getNoInstancesFoundMessage = (value: string) => {
return `Unable to find an element with accessibilityLabel: ${value}`;
return `Unable to find an element with accessibility label: ${value}`;
};

const Typography = ({ children, ...rest }: any) => {
Expand Down Expand Up @@ -161,7 +161,7 @@ test('byLabelText queries support hidden option', () => {
).toBeFalsy();
expect(() => getByLabelText('hidden', { includeHiddenElements: false }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: hidden
"Unable to find an element with accessibility label: hidden

<Text
accessibilityLabel="hidden"
Expand All @@ -176,6 +176,24 @@ test('byLabelText queries support hidden option', () => {
`);
});

test('getByLabelText supports aria-label', async () => {
const screen = render(
<>
<View testID="view" aria-label="view-label" />
<Text testID="text" aria-label="text-label">
Text
</Text>
<TextInput testID="text-input" aria-label="text-input-label" />
</>
);

expect(screen.getByLabelText('view-label')).toBe(screen.getByTestId('view'));
expect(screen.getByLabelText('text-label')).toBe(screen.getByTestId('text'));
expect(screen.getByLabelText('text-input-label')).toBe(
screen.getByTestId('text-input')
);
});

test('getByLabelText supports accessibilityLabelledBy', async () => {
const { getByLabelText, getByTestId } = render(
<>
Expand All @@ -202,11 +220,45 @@ test('getByLabelText supports nested accessibilityLabelledBy', async () => {
expect(getByLabelText(/input/)).toBe(getByTestId('textInput'));
});

test('getByLabelText supports aria-labelledby', async () => {
const screen = render(
<>
<Text nativeID="label">Text Label</Text>
<TextInput testID="text-input" aria-labelledby="label" />
</>
);

expect(screen.getByLabelText('Text Label')).toBe(
screen.getByTestId('text-input')
);
expect(screen.getByLabelText(/text label/i)).toBe(
screen.getByTestId('text-input')
);
});

test('getByLabelText supports nested aria-labelledby', async () => {
const screen = render(
<>
<View nativeID="label">
<Text>Nested Text Label</Text>
</View>
<TextInput testID="text-input" aria-labelledby="label" />
</>
);

expect(screen.getByLabelText('Nested Text Label')).toBe(
screen.getByTestId('text-input')
);
expect(screen.getByLabelText(/nested text label/i)).toBe(
screen.getByTestId('text-input')
);
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(<TouchableOpacity accessibilityLabel="LABEL" key="3" />);

expect(() => view.getByLabelText('FOO')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: FOO
"Unable to find an element with accessibility label: FOO

<View
accessibilityLabel="LABEL"
Expand All @@ -215,7 +267,7 @@ test('error message renders the element tree, preserving only helpful props', as

expect(() => view.getAllByLabelText('FOO'))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: FOO
"Unable to find an element with accessibility label: FOO

<View
accessibilityLabel="LABEL"
Expand All @@ -224,7 +276,7 @@ test('error message renders the element tree, preserving only helpful props', as

await expect(view.findByLabelText('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: FOO
"Unable to find an element with accessibility label: FOO

<View
accessibilityLabel="LABEL"
Expand All @@ -233,7 +285,7 @@ test('error message renders the element tree, preserving only helpful props', as

await expect(view.findAllByLabelText('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: FOO
"Unable to find an element with accessibility label: FOO

<View
accessibilityLabel="LABEL"
Expand Down
16 changes: 10 additions & 6 deletions src/queries/__tests__/makeQueries.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ describe('printing element tree', () => {
test('prints helpful props but not others', async () => {
const { getByText } = render(
<View
aria-hidden
accessibilityElementsHidden
accessibilityViewIsModal
importantForAccessibility="yes"
key="this is filtered"
testID="TEST_ID"
nativeID="NATIVE_ID"
accessibilityElementsHidden
accessibilityLabel="LABEL"
accessibilityLabelledBy="LABELLED_BY"
accessibilityRole="summary"
accessibilityHint="HINT"
key="this is filtered"
accessibilityRole="summary"
accessibilityViewIsModal
aria-hidden
aria-label="ARIA_LABEL"
aria-labelledby="ARIA_LABELLED_BY"
importantForAccessibility="yes"
>
<TextInput
placeholder="PLACEHOLDER"
Expand All @@ -50,6 +52,8 @@ describe('printing element tree', () => {
accessibilityRole="summary"
accessibilityViewIsModal={true}
aria-hidden={true}
aria-label="ARIA_LABEL"
aria-labelledby="ARIA_LABELLED_BY"
importantForAccessibility="yes"
nativeID="NATIVE_ID"
testID="TEST_ID"
Expand Down
28 changes: 28 additions & 0 deletions src/queries/__tests__/role.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,34 @@ describe('supports name option', () => {
);
});

test('returns an element that has the corresponding role and a children with a matching aria-label', () => {
const { getByRole } = render(
<TouchableOpacity accessibilityRole="button" testID="target-button">
<Text aria-label="Save" />
</TouchableOpacity>
);

// assert on the testId to be sure that the returned element is the one with the accessibilityRole
expect(getByRole('button', { name: 'Save' }).props.testID).toBe(
'target-button'
);
});

test('returns an element that has the corresponding role and a matching aria-label', () => {
const { getByRole } = render(
<TouchableOpacity
accessibilityRole="button"
testID="target-button"
aria-label="Save"
></TouchableOpacity>
);

// assert on the testId to be sure that the returned element is the one with the accessibilityRole
expect(getByRole('button', { name: 'Save' }).props.testID).toBe(
'target-button'
);
});

test('returns an element when the direct child is text', () => {
const { getByRole, getByTestId } = render(
<Text accessibilityRole="header" testID="target-header">
Expand Down
4 changes: 2 additions & 2 deletions src/queries/labelText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ function queryAllByLabelText(instance: ReactTestInstance) {
}

const getMultipleError = (labelText: TextMatch) =>
`Found multiple elements with accessibilityLabel: ${String(labelText)} `;
`Found multiple elements with accessibility label: ${String(labelText)} `;
const getMissingError = (labelText: TextMatch) =>
`Unable to find an element with accessibilityLabel: ${String(labelText)}`;
`Unable to find an element with accessibility label: ${String(labelText)}`;

const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
queryAllByLabelText,
Expand Down
8 changes: 4 additions & 4 deletions website/docs/Queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ title: Queries
- [Precision](#precision)
- [Normalization](#normalization)
- [Unit testing helpers](#unit-testing-helpers)
- [`UNSAFE_ByType`](#unsafebytype)
- [`UNSAFE_ByProps`](#unsafebyprops)
- [`UNSAFE_ByType`](#unsafe_bytype)
- [`UNSAFE_ByProps`](#unsafe_byprops)

## Variants

Expand Down Expand Up @@ -272,8 +272,8 @@ getByLabelText(
```

Returns a `ReactTestInstance` with matching label:
- either by matching [`accessibilityLabel`](https://reactnative.dev/docs/accessibility#accessibilitylabel) prop
- or by matching text content of view referenced by [`accessibilityLabelledBy`](https://reactnative.dev/docs/accessibility#accessibilitylabelledby-android) prop
- either by matching [`aria-label`](https://reactnative.dev/docs/accessibility#aria-label)/[`accessibilityLabel`](https://reactnative.dev/docs/accessibility#accessibilitylabel) prop
- 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

```jsx
import { render, screen } from '@testing-library/react-native';
Expand Down