diff --git a/src/helpers/__tests__/format-default.test.tsx b/src/helpers/__tests__/format-default.test.tsx index ab67cd700..917d9a12e 100644 --- a/src/helpers/__tests__/format-default.test.tsx +++ b/src/helpers/__tests__/format-default.test.tsx @@ -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', diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts index 0e9e1b809..7a4bb0e1d 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -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; } @@ -134,7 +136,9 @@ export function getAccessibilityLabelledBy( ); } -export function getAccessibilityState(element: ReactTestInstance) { +export function getAccessibilityState( + element: ReactTestInstance +): AccessibilityState | undefined { const { accessibilityState, 'aria-busy': ariaBusy, @@ -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, + }; +} diff --git a/src/helpers/format-default.ts b/src/helpers/format-default.ts index 0a7752a6a..717bed9ec 100644 --- a/src/helpers/format-default.ts +++ b/src/helpers/format-default.ts @@ -17,6 +17,10 @@ const propsToDisplay = [ 'aria-labelledby', 'aria-modal', 'aria-selected', + 'aria-valuemax', + 'aria-valuemin', + 'aria-valuenow', + 'aria-valuetext', 'defaultValue', 'importantForAccessibility', 'nativeID', diff --git a/src/helpers/matchers/accessibilityValue.ts b/src/helpers/matchers/accessibilityValue.ts index ef72c1cdb..0c8a04c09 100644 --- a/src/helpers/matchers/accessibilityValue.ts +++ b/src/helpers/matchers/accessibilityValue.ts @@ -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'; @@ -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)) ); } diff --git a/src/matchers/__tests__/to-be-checked.test.tsx b/src/matchers/__tests__/to-be-checked.test.tsx index 83f75c3b2..4e7549177 100644 --- a/src/matchers/__tests__/to-be-checked.test.tsx +++ b/src/matchers/__tests__/to-be-checked.test.tsx @@ -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(); + + const view = screen.getByTestId('test'); + expect(() => expect(view).toBeChecked()).toThrowErrorMatchingInlineSnapshot( + `"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."` + ); +}); diff --git a/src/matchers/to-be-checked.tsx b/src/matchers/to-be-checked.tsx index 7a9a67e0d..6e641ef39 100644 --- a/src/matchers/to-be-checked.tsx +++ b/src/matchers/to-be-checked.tsx @@ -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'; } diff --git a/src/queries/__tests__/a11yValue.test.tsx b/src/queries/__tests__/a11yValue.test.tsx index 8cbd2851a..7236cc3ae 100644 --- a/src/queries/__tests__/a11yValue.test.tsx +++ b/src/queries/__tests__/a11yValue.test.tsx @@ -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(); + expect(screen.getByAccessibilityValue({ max: 10 })).toBeTruthy(); + }); + + test('supports "aria-valuemin"', () => { + const screen = render(); + expect(screen.getByAccessibilityValue({ min: 20 })).toBeTruthy(); + }); + + test('supports "aria-valuenow"', () => { + const screen = render(); + expect(screen.getByAccessibilityValue({ now: 30 })).toBeTruthy(); + }); + + test('supports "aria-valuetext"', () => { + const screen = render(); + expect( + screen.getByAccessibilityValue({ text: 'Hello World' }) + ).toBeTruthy(); + expect(screen.getByAccessibilityValue({ text: /hello/i })).toBeTruthy(); + }); + + test('supports multiple "aria-value*" props', () => { + const screen = render( + + ); + expect( + screen.getByAccessibilityValue({ now: 50, min: 0, max: 100 }) + ).toBeTruthy(); + }); +}); diff --git a/src/queries/__tests__/makeQueries.test.tsx b/src/queries/__tests__/makeQueries.test.tsx index 506603263..5b2d39813 100644 --- a/src/queries/__tests__/makeQueries.test.tsx +++ b/src/queries/__tests__/makeQueries.test.tsx @@ -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" diff --git a/src/queries/__tests__/role-value.test.tsx b/src/queries/__tests__/role-value.test.tsx index c1da194fa..eed847111 100644 --- a/src/queries/__tests__/role-value.test.tsx +++ b/src/queries/__tests__/role-value.test.tsx @@ -173,4 +173,55 @@ describe('accessibility value', () => { " `); }); + + test('supports "aria-valuemax" prop', () => { + const screen = render(); + expect(screen.getByRole('slider', { value: { max: 10 } })).toBeTruthy(); + expect(screen.queryByRole('slider', { value: { max: 20 } })).toBeNull(); + }); + + test('supports "aria-valuemin" prop', () => { + const screen = render(); + expect(screen.getByRole('slider', { value: { min: 20 } })).toBeTruthy(); + expect(screen.queryByRole('slider', { value: { min: 30 } })).toBeNull(); + }); + + test('supports "aria-valuenow" prop', () => { + const screen = render(); + expect(screen.getByRole('slider', { value: { now: 30 } })).toBeTruthy(); + expect(screen.queryByRole('slider', { value: { now: 10 } })).toBeNull(); + }); + + test('supports "aria-valuetext" prop', () => { + const screen = render( + + ); + 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( + + ); + expect( + screen.getByRole('slider', { value: { now: 50, min: 0, max: 100 } }) + ).toBeTruthy(); + }); }); diff --git a/website/docs/Queries.md b/website/docs/Queries.md index 25a6aa83a..462752d29 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -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` @@ -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.