diff --git a/src/helpers/__tests__/format-default.test.tsx b/src/helpers/__tests__/format-default.test.tsx index 315db2e16..8f2e784b2 100644 --- a/src/helpers/__tests__/format-default.test.tsx +++ b/src/helpers/__tests__/format-default.test.tsx @@ -20,8 +20,13 @@ describe('mapPropsForQueryError', () => { accessibilityLabelledBy: 'LABELLED_BY', accessibilityRole: 'ROLE', accessibilityHint: 'HINT', + 'aria-busy': 'ARIA-BUSY', + 'aria-checked': 'ARIA-CHECKED', + 'aria-disabled': 'ARIA-DISABLED', + 'aria-expanded': 'ARIA-EXPANDED', 'aria-label': 'ARIA_LABEL', 'aria-labelledby': 'ARIA_LABELLED_BY', + 'aria-selected': 'ARIA-SELECTED', placeholder: 'PLACEHOLDER', value: 'VALUE', defaultValue: 'DEFAULT_VALUE', diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts index 631c16500..b29088ad9 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -129,3 +129,34 @@ export function getAccessibilityLabelledBy( element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy ); } + +export function getAccessibilityState(element: ReactTestInstance) { + const { + accessibilityState, + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-selected': ariaSelected, + } = element.props; + + const hasAnyAccessibilityStateProps = + accessibilityState != null || + ariaBusy != null || + ariaChecked != null || + ariaDisabled != null || + ariaExpanded != null || + ariaSelected != null; + + if (!hasAnyAccessibilityStateProps) { + return undefined; + } + + return { + busy: ariaBusy ?? accessibilityState?.busy, + checked: ariaChecked ?? accessibilityState?.checked, + disabled: ariaDisabled ?? accessibilityState?.disabled, + expanded: ariaExpanded ?? accessibilityState?.expanded, + selected: ariaSelected ?? accessibilityState?.selected, + }; +} diff --git a/src/helpers/format-default.ts b/src/helpers/format-default.ts index fb1fcb62f..1f2faab51 100644 --- a/src/helpers/format-default.ts +++ b/src/helpers/format-default.ts @@ -8,9 +8,14 @@ const propsToDisplay = [ 'accessibilityLabelledBy', 'accessibilityRole', 'accessibilityViewIsModal', + 'aria-busy', + 'aria-checked', + 'aria-disabled', + 'aria-expanded', 'aria-hidden', 'aria-label', 'aria-labelledby', + 'aria-selected', 'defaultValue', 'importantForAccessibility', 'nativeID', diff --git a/src/helpers/matchers/accessibilityState.ts b/src/helpers/matchers/accessibilityState.ts index cd02a9679..4f46d9737 100644 --- a/src/helpers/matchers/accessibilityState.ts +++ b/src/helpers/matchers/accessibilityState.ts @@ -1,6 +1,6 @@ import { AccessibilityState } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; -import { accessibilityStateKeys } from '../accessiblity'; +import { accessibilityStateKeys, getAccessibilityState } from '../accessiblity'; // This type is the same as AccessibilityState from `react-native` package // It is re-declared here due to issues with migration from `@types/react-native` to @@ -32,13 +32,13 @@ export function matchAccessibilityState( node: ReactTestInstance, matcher: AccessibilityStateMatcher ) { - const state = node.props.accessibilityState; - return accessibilityStateKeys.every((key) => matchState(state, matcher, key)); + const state = getAccessibilityState(node); + return accessibilityStateKeys.every((key) => matchState(matcher, state, key)); } function matchState( - state: AccessibilityState, matcher: AccessibilityStateMatcher, + state: AccessibilityState | undefined, key: keyof AccessibilityState ) { return ( diff --git a/src/queries/__tests__/a11yState.test.tsx b/src/queries/__tests__/a11yState.test.tsx index ea3163b46..f3b01cf42 100644 --- a/src/queries/__tests__/a11yState.test.tsx +++ b/src/queries/__tests__/a11yState.test.tsx @@ -437,3 +437,125 @@ test('error message renders the element tree, preserving only helpful props', as " `); }); + +describe('aria-disabled prop', () => { + test('supports aria-disabled={true} prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({ disabled: true })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ disabled: false })).toBeNull(); + }); + + test('supports aria-disabled={false} prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({ disabled: false })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ disabled: true })).toBeNull(); + }); + + test('supports default aria-disabled prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({ disabled: false })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ disabled: true })).toBeNull(); + }); +}); + +describe('aria-selected prop', () => { + test('supports aria-selected={true} prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({ selected: true })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ selected: false })).toBeNull(); + }); + + test('supports aria-selected={false} prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({ selected: false })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ selected: true })).toBeNull(); + }); + + test('supports default aria-selected prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({ selected: false })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ selected: true })).toBeNull(); + }); +}); + +describe('aria-checked prop', () => { + test('supports aria-checked={true} prop', () => { + const screen = render( + + ); + expect(screen.getByAccessibilityState({ checked: true })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ checked: false })).toBeNull(); + expect(screen.queryByAccessibilityState({ checked: 'mixed' })).toBeNull(); + }); + + test('supports aria-checked={false} prop', () => { + const screen = render( + + ); + expect(screen.getByAccessibilityState({ checked: false })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ checked: true })).toBeNull(); + expect(screen.queryByAccessibilityState({ checked: 'mixed' })).toBeNull(); + }); + + test('supports aria-checked="mixed prop', () => { + const screen = render( + + ); + expect(screen.getByAccessibilityState({ checked: 'mixed' })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ checked: true })).toBeNull(); + expect(screen.queryByAccessibilityState({ checked: false })).toBeNull(); + }); + + test('supports default aria-selected prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({})).toBeTruthy(); + expect(screen.queryByAccessibilityState({ checked: true })).toBeNull(); + expect(screen.queryByAccessibilityState({ checked: false })).toBeNull(); + expect(screen.queryByAccessibilityState({ checked: 'mixed' })).toBeNull(); + }); +}); + +describe('aria-busy prop', () => { + test('supports aria-busy={true} prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({ busy: true })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ busy: false })).toBeNull(); + }); + + test('supports aria-busy={false} prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({ busy: false })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ busy: true })).toBeNull(); + }); + + test('supports default aria-busy prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({ busy: false })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ busy: true })).toBeNull(); + }); +}); + +describe('aria-expanded prop', () => { + test('supports aria-expanded={true} prop', () => { + const screen = render( + + ); + expect(screen.getByAccessibilityState({ expanded: true })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ expanded: false })).toBeNull(); + }); + + test('supports aria-expanded={false} prop', () => { + const screen = render( + + ); + expect(screen.getByAccessibilityState({ expanded: false })).toBeTruthy(); + expect(screen.queryByAccessibilityState({ expanded: true })).toBeNull(); + }); + + test('supports default aria-expanded prop', () => { + const screen = render(); + expect(screen.getByAccessibilityState({})).toBeTruthy(); + expect(screen.queryByAccessibilityState({ expanded: true })).toBeNull(); + expect(screen.queryByAccessibilityState({ expanded: false })).toBeNull(); + }); +}); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index c54fb9832..cb25c284b 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -339,6 +339,28 @@ describe('supports accessibility states', () => { getByRole('button', { name: 'RNButton', disabled: true }) ).toBeTruthy(); }); + + test('supports aria-disabled={true} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { disabled: true })).toBeTruthy(); + expect(screen.queryByRole('button', { disabled: false })).toBeNull(); + }); + + test('supports aria-disabled={false} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { disabled: false })).toBeTruthy(); + expect(screen.queryByRole('button', { disabled: true })).toBeNull(); + }); + + test('supports default aria-disabled prop', () => { + const screen = render(); + expect(screen.getByRole('button', { disabled: false })).toBeTruthy(); + expect(screen.queryByRole('button', { disabled: true })).toBeNull(); + }); }); describe('selected', () => { @@ -406,6 +428,28 @@ describe('supports accessibility states', () => { expect(queryByRole('tab', { selected: false })).toBe(null); }); + + test('supports aria-selected={true} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { selected: true })).toBeTruthy(); + expect(screen.queryByRole('button', { selected: false })).toBeNull(); + }); + + test('supports aria-selected={false} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { selected: false })).toBeTruthy(); + expect(screen.queryByRole('button', { selected: true })).toBeNull(); + }); + + test('supports default aria-selected prop', () => { + const screen = render(); + expect(screen.getByRole('button', { selected: false })).toBeTruthy(); + expect(screen.queryByRole('button', { selected: true })).toBeNull(); + }); }); describe('checked', () => { @@ -508,6 +552,41 @@ describe('supports accessibility states', () => { expect(queryByRole('checkbox', { checked: false })).toBe(null); }); + + test('supports aria-checked={true} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { checked: true })).toBeTruthy(); + expect(screen.queryByRole('button', { checked: false })).toBeNull(); + expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull(); + }); + + test('supports aria-checked={false} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { checked: false })).toBeTruthy(); + expect(screen.queryByRole('button', { checked: true })).toBeNull(); + expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull(); + }); + + test('supports aria-checked="mixed prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { checked: 'mixed' })).toBeTruthy(); + expect(screen.queryByRole('button', { checked: true })).toBeNull(); + expect(screen.queryByRole('button', { checked: false })).toBeNull(); + }); + + test('supports default aria-selected prop', () => { + const screen = render(); + expect(screen.getByRole('button')).toBeTruthy(); + expect(screen.queryByRole('button', { checked: true })).toBeNull(); + expect(screen.queryByRole('button', { checked: false })).toBeNull(); + expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull(); + }); }); describe('busy', () => { @@ -575,6 +654,28 @@ describe('supports accessibility states', () => { expect(queryByRole('button', { selected: false })).toBe(null); }); + + test('supports aria-busy={true} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { busy: true })).toBeTruthy(); + expect(screen.queryByRole('button', { busy: false })).toBeNull(); + }); + + test('supports aria-busy={false} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { busy: false })).toBeTruthy(); + expect(screen.queryByRole('button', { busy: true })).toBeNull(); + }); + + test('supports default aria-busy prop', () => { + const screen = render(); + expect(screen.getByRole('button', { busy: false })).toBeTruthy(); + expect(screen.queryByRole('button', { busy: true })).toBeNull(); + }); }); describe('expanded', () => { @@ -641,6 +742,29 @@ describe('supports accessibility states', () => { expect(queryByRole('button', { expanded: false })).toBe(null); }); + + test('supports aria-expanded={true} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { expanded: true })).toBeTruthy(); + expect(screen.queryByRole('button', { expanded: false })).toBeNull(); + }); + + test('supports aria-expanded={false} prop', () => { + const screen = render( + + ); + expect(screen.getByRole('button', { expanded: false })).toBeTruthy(); + expect(screen.queryByRole('button', { expanded: true })).toBeNull(); + }); + + test('supports default aria-expanded prop', () => { + const screen = render(); + expect(screen.getByRole('button')).toBeTruthy(); + expect(screen.queryByRole('button', { expanded: true })).toBeNull(); + expect(screen.queryByRole('button', { expanded: false })).toBeNull(); + }); }); test('ignores non queried accessibilityState', () => { diff --git a/website/docs/Queries.md b/website/docs/Queries.md index 2a6c37ab7..e99cc5b8b 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -30,8 +30,8 @@ title: Queries - [Precision](#precision) - [Normalization](#normalization) - [Unit testing helpers](#unit-testing-helpers) - - [`UNSAFE_ByType`](#unsafe_bytype) - - [`UNSAFE_ByProps`](#unsafe_byprops) + - [`UNSAFE_ByType`](#unsafebytype) + - [`UNSAFE_ByProps`](#unsafebyprops) ## Variants @@ -142,15 +142,15 @@ const element3 = screen.getByRole('button', { name: 'Hello', disabled: true }); `name`: Finds an element with given `role`/`accessibilityRole` and an accessible name (equivalent to `byText` or `byLabelText` query). -`disabled`: You can filter elements by their disabled state. The possible values are `true` or `false`. Querying `disabled: false` will also match elements with `disabled: undefined` (see the [wiki](https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State) for more details). See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `disabled` state. +`disabled`: You can filter elements by their disabled state (coming either from `aria-disabled` prop or `accessbilityState.disabled` prop). The possible values are `true` or `false`. Querying `disabled: false` will also match elements with `disabled: undefined` (see the [wiki](https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State) for more details). See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `disabled` state. -`selected`: You can filter elements by their selected state. The possible values are `true` or `false`. Querying `selected: false` will also match elements with `selected: undefined` (see the [wiki](https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State) for more details). See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `selected` state. +`selected`: You can filter elements by their selected state (coming either from `aria-selected` prop or `accessbilityState.selected` prop). The possible values are `true` or `false`. Querying `selected: false` will also match elements with `selected: undefined` (see the [wiki](https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State) for more details). See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `selected` state. -`checked`: You can filter elements by their checked state. The possible values are `true`, `false`, or `"mixed"`. See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `checked` state. +`checked`: You can filter elements by their checked state (coming either from `aria-checked` prop or `accessbilityState.checked` prop). The possible values are `true`, `false`, or `"mixed"`. See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `checked` state. -`busy`: You can filter elements by their busy state. The possible values are `true` or `false`. Querying `busy: false` will also match elements with `busy: undefined` (see the [wiki](https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State) for more details). See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `busy` state. +`busy`: You can filter elements by their busy state (coming either from `aria-busy` prop or `accessbilityState.busy` prop). The possible values are `true` or `false`. Querying `busy: false` will also match elements with `busy: undefined` (see the [wiki](https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State) for more details). See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `busy` state. -`expanded`: You can filter elements by their expanded state. 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. +`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. @@ -331,8 +331,8 @@ getByA11yState( disabled?: boolean, selected?: boolean, checked?: boolean | 'mixed', - expanded?: boolean, busy?: boolean, + expanded?: boolean, }, options?: { includeHiddenElements?: boolean; @@ -340,7 +340,7 @@ getByA11yState( ): ReactTestInstance; ``` -Returns a `ReactTestInstance` with matching `accessibilityState` prop. +Returns a `ReactTestInstance` with matching `accessibilityState` prop or ARIA state props: `aria-disabled`, `aria-selected`, `aria-checked`, `aria-busy`, and `aria-expanded`. ```jsx import { render, screen } from '@testing-library/react-native';