Skip to content

feat: aria-value* props support #1480

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

Merged
merged 3 commits into from
Sep 6, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/helpers/__tests__/format-default.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ describe('mapPropsForQueryError', () => {
'aria-labelledby': 'ARIA_LABELLED_BY',
'aria-modal': true,
'aria-selected': 'ARIA-SELECTED',
'aria-valuemax': 'ARIA-VALUEMAX',
'aria-valuemin': 'ARIA-VALUEMIN',
'aria-valuenow': 'ARIA-VALUENOW',
'aria-valuetext': 'ARIA-VALUETEXT',
placeholder: 'PLACEHOLDER',
value: 'VALUE',
defaultValue: 'DEFAULT_VALUE',
Expand Down
38 changes: 36 additions & 2 deletions src/helpers/accessiblity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ export function isAccessibilityElement(
);
}

export function getAccessibilityRole(element: ReactTestInstance) {
export function getAccessibilityRole(
element: ReactTestInstance
): string | undefined {
return element.props.role ?? element.props.accessibilityRole;
}

Expand All @@ -134,7 +136,9 @@ export function getAccessibilityLabelledBy(
);
}

export function getAccessibilityState(element: ReactTestInstance) {
export function getAccessibilityState(
element: ReactTestInstance
): AccessibilityState | undefined {
const {
accessibilityState,
'aria-busy': ariaBusy,
Expand Down Expand Up @@ -171,3 +175,33 @@ export function getAccessibilityCheckedState(
const { accessibilityState, 'aria-checked': ariaChecked } = element.props;
return ariaChecked ?? accessibilityState?.checked;
}

export function getAccessibilityValue(
element: ReactTestInstance
): AccessibilityValue | undefined {
const {
accessibilityValue,
'aria-valuemax': ariaValueMax,
'aria-valuemin': ariaValueMin,
'aria-valuenow': ariaValueNow,
'aria-valuetext': ariaValueText,
} = element.props;

const hasAnyAccessibilityValueProps =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: here I think that a condition hasNoAccessibilityValueProps would be a bit clearer because it would remove the negation in the next if

accessibilityValue != null ||
ariaValueMax != null ||
ariaValueMin != null ||
ariaValueNow != null ||
ariaValueText != null;

if (!hasAnyAccessibilityValueProps) {
return undefined;
}

return {
max: ariaValueMax ?? accessibilityValue?.max,
min: ariaValueMin ?? accessibilityValue?.min,
now: ariaValueNow ?? accessibilityValue?.now,
text: ariaValueText ?? accessibilityValue?.text,
};
}
4 changes: 4 additions & 0 deletions src/helpers/format-default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const propsToDisplay = [
'aria-labelledby',
'aria-modal',
'aria-selected',
'aria-valuemax',
'aria-valuemin',
'aria-valuenow',
'aria-valuetext',
'defaultValue',
'importantForAccessibility',
'nativeID',
Expand Down
12 changes: 6 additions & 6 deletions src/helpers/matchers/accessibilityValue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AccessibilityValue } from 'react-native';
import { ReactTestInstance } from 'react-test-renderer';
import { getAccessibilityValue } from '../accessiblity';
import { TextMatch } from '../../matches';
import { matchStringProp } from './matchStringProp';

Expand All @@ -14,11 +14,11 @@ export function matchAccessibilityValue(
node: ReactTestInstance,
matcher: AccessibilityValueMatcher
): boolean {
const value: AccessibilityValue = node.props.accessibilityValue ?? {};
const value = getAccessibilityValue(node);
return (
(matcher.min === undefined || matcher.min === value.min) &&
(matcher.max === undefined || matcher.max === value.max) &&
(matcher.now === undefined || matcher.now === value.now) &&
(matcher.text === undefined || matchStringProp(value.text, matcher.text))
(matcher.min === undefined || matcher.min === value?.min) &&
(matcher.max === undefined || matcher.max === value?.max) &&
(matcher.now === undefined || matcher.now === value?.now) &&
(matcher.text === undefined || matchStringProp(value?.text, matcher.text))
);
}
9 changes: 9 additions & 0 deletions src/matchers/__tests__/to-be-checked.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,12 @@ test('throws error for invalid role', () => {
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`
);
});

test('throws error for non-accessibility element', () => {
render(<View testID="test" />);

const view = screen.getByTestId('test');
expect(() => expect(view).toBeChecked()).toThrowErrorMatchingInlineSnapshot(
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`
);
});
8 changes: 5 additions & 3 deletions src/matchers/to-be-checked.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ export function toBeChecked(
};
}

const VALID_ROLES = new Set(['checkbox', 'radio']);

function hasValidAccessibilityRole(element: ReactTestInstance) {
if (!isAccessibilityElement(element)) {
return false;
}

const role = getAccessibilityRole(element);
return isAccessibilityElement(element) && VALID_ROLES.has(role);
return role === 'checkbox' || role === 'radio';
}
34 changes: 34 additions & 0 deletions src/queries/__tests__/a11yValue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,37 @@ test('error message renders the element tree, preserving only helpful props', as
/>"
`);
});

describe('getByAccessibilityValue supports "aria-*" props', () => {
test('supports "aria-valuemax"', () => {
const screen = render(<View aria-valuemax={10} />);
expect(screen.getByAccessibilityValue({ max: 10 })).toBeTruthy();
});

test('supports "aria-valuemin"', () => {
const screen = render(<View aria-valuemin={20} />);
expect(screen.getByAccessibilityValue({ min: 20 })).toBeTruthy();
});

test('supports "aria-valuenow"', () => {
const screen = render(<View aria-valuenow={30} />);
expect(screen.getByAccessibilityValue({ now: 30 })).toBeTruthy();
});

test('supports "aria-valuetext"', () => {
const screen = render(<View aria-valuetext="Hello World" />);
expect(
screen.getByAccessibilityValue({ text: 'Hello World' })
).toBeTruthy();
expect(screen.getByAccessibilityValue({ text: /hello/i })).toBeTruthy();
});

test('supports multiple "aria-value*" props', () => {
const screen = render(
<View aria-valuenow={50} aria-valuemin={0} aria-valuemax={100} />
);
expect(
screen.getByAccessibilityValue({ now: 50, min: 0, max: 100 })
).toBeTruthy();
});
});
4 changes: 4 additions & 0 deletions src/queries/__tests__/makeQueries.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ describe('printing element tree', () => {
aria-labelledby="ARIA_LABELLED_BY"
aria-modal={true}
aria-selected={false}
aria-valuemax={30}
aria-valuemin={10}
aria-valuenow={20}
aria-valuetext="Hello Value"
importantForAccessibility="yes"
nativeID="NATIVE_ID"
role="summary"
Expand Down
51 changes: 51 additions & 0 deletions src/queries/__tests__/role-value.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,55 @@ describe('accessibility value', () => {
</Text>"
`);
});

test('supports "aria-valuemax" prop', () => {
const screen = render(<View accessible role="slider" aria-valuemax={10} />);
expect(screen.getByRole('slider', { value: { max: 10 } })).toBeTruthy();
expect(screen.queryByRole('slider', { value: { max: 20 } })).toBeNull();
});

test('supports "aria-valuemin" prop', () => {
const screen = render(<View accessible role="slider" aria-valuemin={20} />);
expect(screen.getByRole('slider', { value: { min: 20 } })).toBeTruthy();
expect(screen.queryByRole('slider', { value: { min: 30 } })).toBeNull();
});

test('supports "aria-valuenow" prop', () => {
const screen = render(<View accessible role="slider" aria-valuenow={30} />);
expect(screen.getByRole('slider', { value: { now: 30 } })).toBeTruthy();
expect(screen.queryByRole('slider', { value: { now: 10 } })).toBeNull();
});

test('supports "aria-valuetext" prop', () => {
const screen = render(
<View accessible role="slider" aria-valuetext="Hello World" />
);
expect(
screen.getByRole('slider', { value: { text: 'Hello World' } })
).toBeTruthy();
expect(
screen.getByRole('slider', { value: { text: /hello/i } })
).toBeTruthy();
expect(
screen.queryByRole('slider', { value: { text: 'Hello' } })
).toBeNull();
expect(
screen.queryByRole('slider', { value: { text: /salut/i } })
).toBeNull();
});

test('supports multiple "aria-value*" props', () => {
const screen = render(
<View
accessible
role="slider"
aria-valuenow={50}
aria-valuemin={0}
aria-valuemax={100}
/>
);
expect(
screen.getByRole('slider', { value: { now: 50, min: 0, max: 100 } })
).toBeTruthy();
});
});
4 changes: 2 additions & 2 deletions website/docs/Queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const element3 = screen.getByRole('button', { name: 'Hello', disabled: true });

`expanded`: You can filter elements by their expanded state (coming either from `aria-expanded` prop or `accessbilityState.expanded` prop). The possible values are `true` or `false`. See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `expanded` state.

`value`: Filter elements by their accessibility, available value entries include numeric `min`, `max` & `now`, as well as string or regex `text` key. See React Native [accessibilityValue](https://reactnative.dev/docs/accessibility#accessibilityvalue) docs to learn more about this prop.
`value`: Filter elements by their accessibility value, based on either `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` or `accessibilityValue` props. Accessiblity value conceptually consists of numeric `min`, `max` and `now` entries, as well as string `text` entry. See React Native [accessibilityValue](https://reactnative.dev/docs/accessibility#accessibilityvalue) docs to learn more about the accessibility value concept.

### `ByText`

Expand Down Expand Up @@ -371,7 +371,7 @@ getByA11yValue(
): ReactTestInstance;
```

Returns a host element with matching `accessibilityValue` prop entries. Only entires provided to the query will be used to match elements. Element might have additional accessibility value entries and still be matched.
Returns a host element with matching accessibility value based on `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` & `accessibilityValue` props. Only value entires provided to the query will be used to match elements. Element might have additional accessibility value entries and still be matched.

When querying by `text` entry a string or regex might be used.

Expand Down