Skip to content

Commit

Permalink
feat: aria-value* props support (#1480)
Browse files Browse the repository at this point in the history
* feat: aria-value* props support

* refactor: tweaks

* chore: fix code cov
  • Loading branch information
mdjastrzebski authored Sep 6, 2023
1 parent 5dbd04f commit 3fa6c45
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 13 deletions.
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 =
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

0 comments on commit 3fa6c45

Please sign in to comment.