diff --git a/.vscode/settings.json b/.vscode/settings.json index 0e9990b8a..09b7f95be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["Pressable", "RNTL", "Uncapitalize"] + "cSpell.words": ["labelledby", "Pressable", "RNTL", "Uncapitalize", "valuenow", "valuetext"] } diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 5e031761f..141d45ead 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -1,8 +1,15 @@ -import { AccessibilityState, AccessibilityValue, StyleSheet } from 'react-native'; +import { + AccessibilityRole, + AccessibilityState, + AccessibilityValue, + Role, + StyleSheet, +} from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; import { getHostSiblings, getUnsafeRootElement } from './component-tree'; -import { getHostComponentNames, isHostText } from './host-component-names'; +import { getHostComponentNames, isHostText, isHostTextInput } from './host-component-names'; import { getTextContent } from './text-content'; +import { isTextInputEditable } from './text-input'; type IsInaccessibleOptions = { cache?: WeakMap; @@ -45,7 +52,7 @@ export function isHiddenFromAccessibility( return false; } -/** RTL-compatitibility alias for `isHiddenFromAccessibility` */ +/** RTL-compatibility alias for `isHiddenFromAccessibility` */ export const isInaccessible = isHiddenFromAccessibility; function isSubtreeInaccessible(element: ReactTestInstance): boolean { @@ -78,7 +85,7 @@ function isSubtreeInaccessible(element: ReactTestInstance): boolean { // iOS: accessibilityViewIsModal or aria-modal // See: https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios const hostSiblings = getHostSiblings(element); - if (hostSiblings.some((sibling) => getAccessibilityViewIsModal(sibling))) { + if (hostSiblings.some((sibling) => computeAriaModal(sibling))) { return true; } @@ -115,7 +122,7 @@ export function isAccessibilityElement(element: ReactTestInstance | null): boole * @param element * @returns */ -export function getAccessibilityRole(element: ReactTestInstance) { +export function getRole(element: ReactTestInstance): Role | AccessibilityRole { const explicitRole = element.props.role ?? element.props.accessibilityRole; if (explicitRole) { return explicitRole; @@ -128,57 +135,55 @@ export function getAccessibilityRole(element: ReactTestInstance) { return 'none'; } -export function getAccessibilityViewIsModal(element: ReactTestInstance) { +export function computeAriaModal(element: ReactTestInstance): boolean | undefined { return element.props['aria-modal'] ?? element.props.accessibilityViewIsModal; } -export function getAccessibilityLabel(element: ReactTestInstance): string | undefined { +export function computeAriaLabel(element: ReactTestInstance): string | undefined { return element.props['aria-label'] ?? element.props.accessibilityLabel; } -export function getAccessibilityLabelledBy(element: ReactTestInstance): string | undefined { +export function computeAriaLabelledBy(element: ReactTestInstance): string | undefined { return element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy; } -export function getAccessibilityState(element: ReactTestInstance): AccessibilityState | undefined { - 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; +// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#busy-state +export function computeAriaBusy({ props }: ReactTestInstance): boolean { + return props['aria-busy'] ?? props.accessibilityState?.busy ?? false; +} - if (!hasAnyAccessibilityStateProps) { +// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#checked-state +export function computeAriaChecked(element: ReactTestInstance): AccessibilityState['checked'] { + const role = getRole(element); + if (role !== 'checkbox' && role !== 'radio') { return undefined; } - return { - busy: ariaBusy ?? accessibilityState?.busy, - checked: ariaChecked ?? accessibilityState?.checked, - disabled: ariaDisabled ?? accessibilityState?.disabled, - expanded: ariaExpanded ?? accessibilityState?.expanded, - selected: ariaSelected ?? accessibilityState?.selected, - }; + const props = element.props; + return props['aria-checked'] ?? props.accessibilityState?.checked; +} + +// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#disabled-state +export function computeAriaDisabled(element: ReactTestInstance): boolean { + if (isHostTextInput(element) && !isTextInputEditable(element)) { + return true; + } + + const { props } = element; + return props['aria-disabled'] ?? props.accessibilityState?.disabled ?? false; +} + +// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#expanded-state +export function computeAriaExpanded({ props }: ReactTestInstance): boolean | undefined { + return props['aria-expanded'] ?? props.accessibilityState?.expanded; } -export function getAccessibilityCheckedState( - element: ReactTestInstance, -): AccessibilityState['checked'] { - const { accessibilityState, 'aria-checked': ariaChecked } = element.props; - return ariaChecked ?? accessibilityState?.checked; +// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#selected-state +export function computeAriaSelected({ props }: ReactTestInstance): boolean { + return props['aria-selected'] ?? props.accessibilityState?.selected ?? false; } -export function getAccessibilityValue(element: ReactTestInstance): AccessibilityValue | undefined { +export function computeAriaValue(element: ReactTestInstance): AccessibilityValue { const { accessibilityValue, 'aria-valuemax': ariaValueMax, @@ -187,17 +192,6 @@ export function getAccessibilityValue(element: ReactTestInstance): Accessibility '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, @@ -206,39 +200,13 @@ export function getAccessibilityValue(element: ReactTestInstance): Accessibility }; } -export function isElementBusy(element: ReactTestInstance): NonNullable { - const { accessibilityState, 'aria-busy': ariaBusy } = element.props; - return ariaBusy ?? accessibilityState?.busy ?? false; -} - -export function isElementCollapsed( - element: ReactTestInstance, -): NonNullable { - const { accessibilityState, 'aria-expanded': ariaExpanded } = element.props; - return (ariaExpanded ?? accessibilityState?.expanded) === false; -} - -export function isElementExpanded( - element: ReactTestInstance, -): NonNullable { - const { accessibilityState, 'aria-expanded': ariaExpanded } = element.props; - return ariaExpanded ?? accessibilityState?.expanded ?? false; -} - -export function isElementSelected( - element: ReactTestInstance, -): NonNullable { - const { accessibilityState, 'aria-selected': ariaSelected } = element.props; - return ariaSelected ?? accessibilityState?.selected ?? false; -} - -export function getAccessibleName(element: ReactTestInstance): string | undefined { - const label = getAccessibilityLabel(element); +export function computeAccessibleName(element: ReactTestInstance): string | undefined { + const label = computeAriaLabel(element); if (label) { return label; } - const labelElementId = getAccessibilityLabelledBy(element); + const labelElementId = computeAriaLabelledBy(element); if (labelElementId) { const rootElement = getUnsafeRootElement(element); const labelElement = rootElement?.findByProps({ nativeID: labelElementId }); diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx index c07623e8f..cd4b52239 100644 --- a/src/helpers/host-component-names.tsx +++ b/src/helpers/host-component-names.tsx @@ -70,7 +70,7 @@ function getByTestId(instance: ReactTestInstance, testID: string) { } /** - * Checks if the given element is a host Text. + * Checks if the given element is a host Text element. * @param element The element to check. */ export function isHostText(element?: ReactTestInstance | null): element is HostTestInstance { @@ -78,7 +78,7 @@ export function isHostText(element?: ReactTestInstance | null): element is HostT } /** - * Checks if the given element is a host TextInput. + * Checks if the given element is a host TextInput element. * @param element The element to check. */ export function isHostTextInput(element?: ReactTestInstance | null): element is HostTestInstance { @@ -86,7 +86,15 @@ export function isHostTextInput(element?: ReactTestInstance | null): element is } /** - * Checks if the given element is a host ScrollView. + * Checks if the given element is a host Switch element. + * @param element The element to check. + */ +export function isHostSwitch(element?: ReactTestInstance | null): element is HostTestInstance { + return element?.type === getHostComponentNames().switch; +} + +/** + * Checks if the given element is a host ScrollView element. * @param element The element to check. */ export function isHostScrollView(element?: ReactTestInstance | null): element is HostTestInstance { @@ -94,7 +102,7 @@ export function isHostScrollView(element?: ReactTestInstance | null): element is } /** - * Checks if the given element is a host Modal. + * Checks if the given element is a host Modal element. * @param element The element to check. */ export function isHostModal(element?: ReactTestInstance | null): element is HostTestInstance { diff --git a/src/helpers/matchers/match-accessibility-state.ts b/src/helpers/matchers/match-accessibility-state.ts index 17ba1d305..099300db3 100644 --- a/src/helpers/matchers/match-accessibility-state.ts +++ b/src/helpers/matchers/match-accessibility-state.ts @@ -1,6 +1,11 @@ -import { AccessibilityState } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; -import { accessibilityStateKeys, getAccessibilityState } from '../accessibility'; +import { + computeAriaBusy, + computeAriaChecked, + computeAriaDisabled, + computeAriaExpanded, + computeAriaSelected, +} from '../accessibility'; // 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 @@ -14,32 +19,25 @@ export interface AccessibilityStateMatcher { expanded?: boolean; } -/** - * Default accessibility state values based on experiments using accessibility - * inspector/screen reader on iOS and Android. - * - * @see https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State - */ -const defaultState: AccessibilityState = { - disabled: false, - selected: false, - checked: undefined, - busy: false, - expanded: undefined, -}; - export function matchAccessibilityState( node: ReactTestInstance, matcher: AccessibilityStateMatcher, ) { - const state = getAccessibilityState(node); - return accessibilityStateKeys.every((key) => matchState(matcher, state, key)); -} + if (matcher.busy !== undefined && matcher.busy !== computeAriaBusy(node)) { + return false; + } + if (matcher.checked !== undefined && matcher.checked !== computeAriaChecked(node)) { + return false; + } + if (matcher.disabled !== undefined && matcher.disabled !== computeAriaDisabled(node)) { + return false; + } + if (matcher.expanded !== undefined && matcher.expanded !== computeAriaExpanded(node)) { + return false; + } + if (matcher.selected !== undefined && matcher.selected !== computeAriaSelected(node)) { + return false; + } -function matchState( - matcher: AccessibilityStateMatcher, - state: AccessibilityState | undefined, - key: keyof AccessibilityState, -) { - return matcher[key] === undefined || matcher[key] === (state?.[key] ?? defaultState[key]); + return true; } diff --git a/src/helpers/matchers/match-accessibility-value.ts b/src/helpers/matchers/match-accessibility-value.ts index bd93b63df..c9370166c 100644 --- a/src/helpers/matchers/match-accessibility-value.ts +++ b/src/helpers/matchers/match-accessibility-value.ts @@ -1,5 +1,5 @@ import { ReactTestInstance } from 'react-test-renderer'; -import { getAccessibilityValue } from '../accessibility'; +import { computeAriaValue } from '../accessibility'; import { TextMatch } from '../../matches'; import { matchStringProp } from './match-string-prop'; @@ -14,7 +14,7 @@ export function matchAccessibilityValue( node: ReactTestInstance, matcher: AccessibilityValueMatcher, ): boolean { - const value = getAccessibilityValue(node); + const value = computeAriaValue(node); return ( (matcher.min === undefined || matcher.min === value?.min) && (matcher.max === undefined || matcher.max === value?.max) && diff --git a/src/helpers/matchers/match-label-text.ts b/src/helpers/matchers/match-label-text.ts index aca584afd..2aa5b9e55 100644 --- a/src/helpers/matchers/match-label-text.ts +++ b/src/helpers/matchers/match-label-text.ts @@ -1,6 +1,6 @@ import { ReactTestInstance } from 'react-test-renderer'; import { matches, TextMatch, TextMatchOptions } from '../../matches'; -import { getAccessibilityLabel, getAccessibilityLabelledBy } from '../accessibility'; +import { computeAriaLabel, computeAriaLabelledBy } from '../accessibility'; import { findAll } from '../find-all'; import { matchTextContent } from './match-text-content'; @@ -12,7 +12,7 @@ export function matchLabelText( ) { return ( matchAccessibilityLabel(element, expectedText, options) || - matchAccessibilityLabelledBy(root, getAccessibilityLabelledBy(element), expectedText, options) + matchAccessibilityLabelledBy(root, computeAriaLabelledBy(element), expectedText, options) ); } @@ -21,7 +21,7 @@ function matchAccessibilityLabel( extpectedLabel: TextMatch, options: TextMatchOptions, ) { - return matches(extpectedLabel, getAccessibilityLabel(element), options.normalizer, options.exact); + return matches(extpectedLabel, computeAriaLabel(element), options.normalizer, options.exact); } function matchAccessibilityLabelledBy( diff --git a/src/matchers/__tests__/to-be-checked.test.tsx b/src/matchers/__tests__/to-be-checked.test.tsx index 7b91130f9..872a08ae8 100644 --- a/src/matchers/__tests__/to-be-checked.test.tsx +++ b/src/matchers/__tests__/to-be-checked.test.tsx @@ -5,7 +5,7 @@ import { screen } from '../../screen'; import '../extend-expect'; function renderViewsWithRole(role: AccessibilityRole) { - return render( + render( <> { - render( - <> - - - - - - , - ); - - expect(screen.getByTestId('expanded')).not.toBeCollapsed(); - expect(screen.getByTestId('expanded-aria')).not.toBeCollapsed(); - expect(screen.getByTestId('not-expanded')).toBeCollapsed(); - expect(screen.getByTestId('not-expanded-aria')).toBeCollapsed(); - expect(screen.getByTestId('default')).not.toBeCollapsed(); -}); - -test('toBeCollapsed() error messages', () => { - render( - <> - - - - - - , - ); - - expect(() => expect(screen.getByTestId('expanded')).toBeCollapsed()) - .toThrowErrorMatchingInlineSnapshot(` - "expect(element).toBeCollapsed() - - Received element is not collapsed: - " - `); - - expect(() => expect(screen.getByTestId('expanded-aria')).toBeCollapsed()) - .toThrowErrorMatchingInlineSnapshot(` - "expect(element).toBeCollapsed() - - Received element is not collapsed: - " - `); - - expect(() => expect(screen.getByTestId('not-expanded')).not.toBeCollapsed()) - .toThrowErrorMatchingInlineSnapshot(` - "expect(element).not.toBeCollapsed() - - Received element is collapsed: - " - `); - - expect(() => expect(screen.getByTestId('not-expanded-aria')).not.toBeCollapsed()) - .toThrowErrorMatchingInlineSnapshot(` - "expect(element).not.toBeCollapsed() - - Received element is collapsed: - " - `); - - expect(() => expect(screen.getByTestId('default')).toBeCollapsed()) - .toThrowErrorMatchingInlineSnapshot(` - "expect(element).toBeCollapsed() - - Received element is not collapsed: - " - `); -}); diff --git a/src/matchers/__tests__/to-be-expanded.test.tsx b/src/matchers/__tests__/to-be-expanded.test.tsx index 200ae2552..b82dd6ed7 100644 --- a/src/matchers/__tests__/to-be-expanded.test.tsx +++ b/src/matchers/__tests__/to-be-expanded.test.tsx @@ -94,3 +94,95 @@ test('toBeExpanded() error messages', () => { />" `); }); + +test('toBeCollapsed() basic case', () => { + render( + <> + + + + + + , + ); + + expect(screen.getByTestId('expanded')).not.toBeCollapsed(); + expect(screen.getByTestId('expanded-aria')).not.toBeCollapsed(); + expect(screen.getByTestId('not-expanded')).toBeCollapsed(); + expect(screen.getByTestId('not-expanded-aria')).toBeCollapsed(); + expect(screen.getByTestId('default')).not.toBeCollapsed(); +}); + +test('toBeCollapsed() error messages', () => { + render( + <> + + + + + + , + ); + + expect(() => expect(screen.getByTestId('expanded')).toBeCollapsed()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeCollapsed() + + Received element is not collapsed: + " + `); + + expect(() => expect(screen.getByTestId('expanded-aria')).toBeCollapsed()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeCollapsed() + + Received element is not collapsed: + " + `); + + expect(() => expect(screen.getByTestId('not-expanded')).not.toBeCollapsed()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeCollapsed() + + Received element is collapsed: + " + `); + + expect(() => expect(screen.getByTestId('not-expanded-aria')).not.toBeCollapsed()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeCollapsed() + + Received element is collapsed: + " + `); + + expect(() => expect(screen.getByTestId('default')).toBeCollapsed()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeCollapsed() + + Received element is not collapsed: + " + `); +}); diff --git a/src/matchers/extend-expect.ts b/src/matchers/extend-expect.ts index ec19feb5d..a96e904c1 100644 --- a/src/matchers/extend-expect.ts +++ b/src/matchers/extend-expect.ts @@ -1,10 +1,9 @@ import { toBeOnTheScreen } from './to-be-on-the-screen'; import { toBeChecked } from './to-be-checked'; -import { toBeCollapsed } from './to-be-collapsed'; import { toBeDisabled, toBeEnabled } from './to-be-disabled'; import { toBeBusy } from './to-be-busy'; import { toBeEmptyElement } from './to-be-empty-element'; -import { toBeExpanded } from './to-be-expanded'; +import { toBeExpanded, toBeCollapsed } from './to-be-expanded'; import { toBePartiallyChecked } from './to-be-partially-checked'; import { toBeSelected } from './to-be-selected'; import { toBeVisible } from './to-be-visible'; diff --git a/src/matchers/index.ts b/src/matchers/index.ts index 23af0223c..914e23ea9 100644 --- a/src/matchers/index.ts +++ b/src/matchers/index.ts @@ -1,9 +1,8 @@ export { toBeBusy } from './to-be-busy'; export { toBeChecked } from './to-be-checked'; -export { toBeCollapsed } from './to-be-collapsed'; export { toBeDisabled, toBeEnabled } from './to-be-disabled'; export { toBeEmptyElement } from './to-be-empty-element'; -export { toBeExpanded } from './to-be-expanded'; +export { toBeExpanded, toBeCollapsed } from './to-be-expanded'; export { toBeOnTheScreen } from './to-be-on-the-screen'; export { toBePartiallyChecked } from './to-be-partially-checked'; export { toBeSelected } from './to-be-selected'; diff --git a/src/matchers/to-be-busy.tsx b/src/matchers/to-be-busy.tsx index 6bc01f5f5..effc027c1 100644 --- a/src/matchers/to-be-busy.tsx +++ b/src/matchers/to-be-busy.tsx @@ -1,13 +1,13 @@ import { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; -import { isElementBusy } from '../helpers/accessibility'; +import { computeAriaBusy } from '../helpers/accessibility'; import { checkHostElement, formatElement } from './utils'; export function toBeBusy(this: jest.MatcherContext, element: ReactTestInstance) { checkHostElement(element, toBeBusy, this); return { - pass: isElementBusy(element), + pass: computeAriaBusy(element), message: () => { const matcher = matcherHint(`${this.isNot ? '.not' : ''}.toBeBusy`, 'element', ''); return [ diff --git a/src/matchers/to-be-checked.tsx b/src/matchers/to-be-checked.tsx index ddb0c96dd..57defac15 100644 --- a/src/matchers/to-be-checked.tsx +++ b/src/matchers/to-be-checked.tsx @@ -1,10 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; -import { - getAccessibilityCheckedState, - getAccessibilityRole, - isAccessibilityElement, -} from '../helpers/accessibility'; +import { computeAriaChecked, getRole, isAccessibilityElement } from '../helpers/accessibility'; import { ErrorWithStack } from '../helpers/errors'; import { checkHostElement, formatElement } from './utils'; @@ -19,7 +15,7 @@ export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstanc } return { - pass: getAccessibilityCheckedState(element) === true, + pass: computeAriaChecked(element) === true, message: () => { const is = this.isNot ? 'is' : 'is not'; return [ @@ -37,6 +33,6 @@ function hasValidAccessibilityRole(element: ReactTestInstance) { return false; } - const role = getAccessibilityRole(element); + const role = getRole(element); return role === 'checkbox' || role === 'radio'; } diff --git a/src/matchers/to-be-collapsed.tsx b/src/matchers/to-be-collapsed.tsx deleted file mode 100644 index 251fa8b9e..000000000 --- a/src/matchers/to-be-collapsed.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ReactTestInstance } from 'react-test-renderer'; -import { matcherHint } from 'jest-matcher-utils'; -import { isElementCollapsed } from '../helpers/accessibility'; -import { checkHostElement, formatElement } from './utils'; - -export function toBeCollapsed(this: jest.MatcherContext, element: ReactTestInstance) { - checkHostElement(element, toBeCollapsed, this); - - return { - pass: isElementCollapsed(element), - message: () => { - const matcher = matcherHint(`${this.isNot ? '.not' : ''}.toBeCollapsed`, 'element', ''); - return [ - matcher, - '', - `Received element is ${this.isNot ? '' : 'not '}collapsed:`, - formatElement(element), - ].join('\n'); - }, - }; -} diff --git a/src/matchers/to-be-disabled.tsx b/src/matchers/to-be-disabled.tsx index 6d44f3395..3c917e078 100644 --- a/src/matchers/to-be-disabled.tsx +++ b/src/matchers/to-be-disabled.tsx @@ -1,14 +1,13 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; -import { isHostTextInput } from '../helpers/host-component-names'; -import { isTextInputEditable } from '../helpers/text-input'; +import { computeAriaDisabled } from '../helpers/accessibility'; import { getHostParent } from '../helpers/component-tree'; import { checkHostElement, formatElement } from './utils'; export function toBeDisabled(this: jest.MatcherContext, element: ReactTestInstance) { checkHostElement(element, toBeDisabled, this); - const isDisabled = isElementDisabled(element) || isAncestorDisabled(element); + const isDisabled = computeAriaDisabled(element) || isAncestorDisabled(element); return { pass: isDisabled, @@ -27,7 +26,7 @@ export function toBeDisabled(this: jest.MatcherContext, element: ReactTestInstan export function toBeEnabled(this: jest.MatcherContext, element: ReactTestInstance) { checkHostElement(element, toBeEnabled, this); - const isEnabled = !isElementDisabled(element) && !isAncestorDisabled(element); + const isEnabled = !computeAriaDisabled(element) && !isAncestorDisabled(element); return { pass: isEnabled, @@ -43,20 +42,11 @@ export function toBeEnabled(this: jest.MatcherContext, element: ReactTestInstanc }; } -function isElementDisabled(element: ReactTestInstance) { - if (isHostTextInput(element) && !isTextInputEditable(element)) { - return true; - } - - const { accessibilityState, 'aria-disabled': ariaDisabled } = element.props; - return ariaDisabled ?? accessibilityState?.disabled ?? false; -} - function isAncestorDisabled(element: ReactTestInstance): boolean { const parent = getHostParent(element); if (parent == null) { return false; } - return isElementDisabled(parent) || isAncestorDisabled(parent); + return computeAriaDisabled(parent) || isAncestorDisabled(parent); } diff --git a/src/matchers/to-be-expanded.tsx b/src/matchers/to-be-expanded.tsx index 2393a7531..cc0744a79 100644 --- a/src/matchers/to-be-expanded.tsx +++ b/src/matchers/to-be-expanded.tsx @@ -1,13 +1,13 @@ import { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; -import { isElementExpanded } from '../helpers/accessibility'; +import { computeAriaExpanded } from '../helpers/accessibility'; import { checkHostElement, formatElement } from './utils'; export function toBeExpanded(this: jest.MatcherContext, element: ReactTestInstance) { checkHostElement(element, toBeExpanded, this); return { - pass: isElementExpanded(element), + pass: computeAriaExpanded(element) === true, message: () => { const matcher = matcherHint(`${this.isNot ? '.not' : ''}.toBeExpanded`, 'element', ''); return [ @@ -19,3 +19,20 @@ export function toBeExpanded(this: jest.MatcherContext, element: ReactTestInstan }, }; } + +export function toBeCollapsed(this: jest.MatcherContext, element: ReactTestInstance) { + checkHostElement(element, toBeCollapsed, this); + + return { + pass: computeAriaExpanded(element) === false, + message: () => { + const matcher = matcherHint(`${this.isNot ? '.not' : ''}.toBeCollapsed`, 'element', ''); + return [ + matcher, + '', + `Received element is ${this.isNot ? '' : 'not '}collapsed:`, + formatElement(element), + ].join('\n'); + }, + }; +} diff --git a/src/matchers/to-be-partially-checked.tsx b/src/matchers/to-be-partially-checked.tsx index 3e0c97172..975c48e93 100644 --- a/src/matchers/to-be-partially-checked.tsx +++ b/src/matchers/to-be-partially-checked.tsx @@ -1,10 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; -import { - getAccessibilityCheckedState, - getAccessibilityRole, - isAccessibilityElement, -} from '../helpers/accessibility'; +import { computeAriaChecked, getRole, isAccessibilityElement } from '../helpers/accessibility'; import { ErrorWithStack } from '../helpers/errors'; import { checkHostElement, formatElement } from './utils'; @@ -19,7 +15,7 @@ export function toBePartiallyChecked(this: jest.MatcherContext, element: ReactTe } return { - pass: getAccessibilityCheckedState(element) === 'mixed', + pass: computeAriaChecked(element) === 'mixed', message: () => { const is = this.isNot ? 'is' : 'is not'; return [ @@ -33,6 +29,6 @@ export function toBePartiallyChecked(this: jest.MatcherContext, element: ReactTe } function hasValidAccessibilityRole(element: ReactTestInstance) { - const role = getAccessibilityRole(element); + const role = getRole(element); return isAccessibilityElement(element) && role === 'checkbox'; } diff --git a/src/matchers/to-be-selected.ts b/src/matchers/to-be-selected.ts index a73e0a42a..be03cc997 100644 --- a/src/matchers/to-be-selected.ts +++ b/src/matchers/to-be-selected.ts @@ -1,13 +1,13 @@ import { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; -import { isElementSelected } from '../helpers/accessibility'; +import { computeAriaSelected } from '../helpers/accessibility'; import { checkHostElement, formatElement } from './utils'; export function toBeSelected(this: jest.MatcherContext, element: ReactTestInstance) { checkHostElement(element, toBeSelected, this); return { - pass: isElementSelected(element), + pass: computeAriaSelected(element), message: () => { const is = this.isNot ? 'is' : 'is not'; return [ diff --git a/src/matchers/to-have-accessibility-value.tsx b/src/matchers/to-have-accessibility-value.tsx index 4a9a2badf..6241adc28 100644 --- a/src/matchers/to-have-accessibility-value.tsx +++ b/src/matchers/to-have-accessibility-value.tsx @@ -1,6 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, stringify } from 'jest-matcher-utils'; -import { getAccessibilityValue } from '../helpers/accessibility'; +import { computeAriaValue } from '../helpers/accessibility'; import { AccessibilityValueMatcher, matchAccessibilityValue, @@ -15,7 +15,7 @@ export function toHaveAccessibilityValue( ) { checkHostElement(element, toHaveAccessibilityValue, this); - const receivedValue = getAccessibilityValue(element); + const receivedValue = computeAriaValue(element); return { pass: matchAccessibilityValue(element, expectedValue), diff --git a/src/matchers/to-have-accessible-name.tsx b/src/matchers/to-have-accessible-name.tsx index 3a38382d2..fce6ac365 100644 --- a/src/matchers/to-have-accessible-name.tsx +++ b/src/matchers/to-have-accessible-name.tsx @@ -1,6 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; -import { getAccessibleName } from '../helpers/accessibility'; +import { computeAccessibleName } from '../helpers/accessibility'; import { TextMatch, TextMatchOptions, matches } from '../matches'; import { checkHostElement, formatMessage } from './utils'; @@ -12,7 +12,7 @@ export function toHaveAccessibleName( ) { checkHostElement(element, toHaveAccessibleName, this); - const receivedName = getAccessibleName(element); + const receivedName = computeAccessibleName(element); const missingExpectedValue = arguments.length === 1; let pass = false; diff --git a/src/queries/__tests__/accessibility-state.test.tsx b/src/queries/__tests__/accessibility-state.test.tsx index f72ac7a01..58cbc9525 100644 --- a/src/queries/__tests__/accessibility-state.test.tsx +++ b/src/queries/__tests__/accessibility-state.test.tsx @@ -88,7 +88,7 @@ test('getAllByA11yState, queryAllByA11yState, findAllByA11yState', async () => { describe('checked state matching', () => { it('handles true', () => { - render(); + render(); expect(screen.getByA11yState({ checked: true })).toBeTruthy(); expect(screen.queryByA11yState({ checked: 'mixed' })).toBeFalsy(); @@ -96,7 +96,7 @@ describe('checked state matching', () => { }); it('handles mixed', () => { - render(); + render(); expect(screen.getByA11yState({ checked: 'mixed' })).toBeTruthy(); expect(screen.queryByA11yState({ checked: true })).toBeFalsy(); @@ -104,15 +104,15 @@ describe('checked state matching', () => { }); it('handles false', () => { - render(); + render(); expect(screen.getByA11yState({ checked: false })).toBeTruthy(); expect(screen.queryByA11yState({ checked: true })).toBeFalsy(); expect(screen.queryByA11yState({ checked: 'mixed' })).toBeFalsy(); }); - it('handles default', () => { - render(); + it('handles default', () => { + render(); expect(screen.queryByA11yState({ checked: false })).toBeFalsy(); expect(screen.queryByA11yState({ checked: true })).toBeFalsy(); @@ -135,7 +135,7 @@ describe('expanded state matching', () => { expect(screen.queryByA11yState({ expanded: true })).toBeFalsy(); }); - it('handles default', () => { + it('handles default', () => { render(); expect(screen.queryByA11yState({ expanded: false })).toBeFalsy(); @@ -158,7 +158,7 @@ describe('disabled state matching', () => { expect(screen.queryByA11yState({ disabled: true })).toBeFalsy(); }); - it('handles default', () => { + it('handles default', () => { render(); expect(screen.getByA11yState({ disabled: false })).toBeTruthy(); @@ -181,7 +181,7 @@ describe('busy state matching', () => { expect(screen.queryByA11yState({ busy: true })).toBeFalsy(); }); - it('handles default', () => { + it('handles default', () => { render(); expect(screen.getByA11yState({ busy: false })).toBeTruthy(); @@ -204,7 +204,7 @@ describe('selected state matching', () => { expect(screen.queryByA11yState({ selected: true })).toBeFalsy(); }); - it('handles default', () => { + it('handles default', () => { render(); expect(screen.getByA11yState({ selected: false })).toBeTruthy(); @@ -463,28 +463,28 @@ describe('aria-selected prop', () => { describe('aria-checked prop', () => { test('supports aria-checked={true} prop', () => { - render(); + 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', () => { - render(); + 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', () => { - render(); + test('supports aria-checked="mixed" prop', () => { + 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', () => { - render(); + test('supports default aria-checked prop', () => { + render(); expect(screen.getByAccessibilityState({})).toBeTruthy(); expect(screen.queryByAccessibilityState({ checked: true })).toBeNull(); expect(screen.queryByAccessibilityState({ checked: false })).toBeNull(); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index 61287f397..bcb6d65d1 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -509,32 +509,32 @@ describe('supports accessibility states', () => { }); test('supports aria-checked={true} prop', () => { - render(); - expect(screen.getByRole('button', { checked: true })).toBeTruthy(); - expect(screen.queryByRole('button', { checked: false })).toBeNull(); - expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull(); + render(); + expect(screen.getByRole('checkbox', { checked: true })).toBeTruthy(); + expect(screen.queryByRole('checkbox', { checked: false })).toBeNull(); + expect(screen.queryByRole('checkbox', { checked: 'mixed' })).toBeNull(); }); test('supports aria-checked={false} prop', () => { - render(); - expect(screen.getByRole('button', { checked: false })).toBeTruthy(); - expect(screen.queryByRole('button', { checked: true })).toBeNull(); - expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull(); + render(); + expect(screen.getByRole('checkbox', { checked: false })).toBeTruthy(); + expect(screen.queryByRole('checkbox', { checked: true })).toBeNull(); + expect(screen.queryByRole('checkbox', { checked: 'mixed' })).toBeNull(); }); - test('supports aria-checked="mixed prop', () => { - render(); - expect(screen.getByRole('button', { checked: 'mixed' })).toBeTruthy(); - expect(screen.queryByRole('button', { checked: true })).toBeNull(); - expect(screen.queryByRole('button', { checked: false })).toBeNull(); + test('supports aria-checked="mixed" prop', () => { + render(); + expect(screen.getByRole('checkbox', { checked: 'mixed' })).toBeTruthy(); + expect(screen.queryByRole('checkbox', { checked: true })).toBeNull(); + expect(screen.queryByRole('checkbox', { checked: false })).toBeNull(); }); test('supports default aria-selected prop', () => { - 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(); + render(); + expect(screen.getByRole('checkbox')).toBeTruthy(); + expect(screen.queryByRole('checkbox', { checked: true })).toBeNull(); + expect(screen.queryByRole('checkbox', { checked: false })).toBeNull(); + expect(screen.queryByRole('checkbox', { checked: 'mixed' })).toBeNull(); }); }); @@ -728,7 +728,7 @@ describe('supports accessibility states', () => { test('matches an element combining all the options', () => { render( { ); expect( - screen.getByRole('button', { + screen.getByRole('checkbox', { name: 'Save', disabled: true, selected: true, diff --git a/src/queries/role.ts b/src/queries/role.ts index 23eccc9f9..a806bf056 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -3,7 +3,7 @@ import type { AccessibilityRole, Role } from 'react-native'; import { accessibilityStateKeys, accessibilityValueKeys, - getAccessibilityRole, + getRole, isAccessibilityElement, } from '../helpers/accessibility'; import { findAll } from '../helpers/find-all'; @@ -65,7 +65,7 @@ const queryAllByRole = ( (node) => // run the cheapest checks first, and early exit to avoid unneeded computations isAccessibilityElement(node) && - matchStringProp(getAccessibilityRole(node), role) && + matchStringProp(getRole(node), role) && matchAccessibleStateIfNeeded(node, options) && matchAccessibilityValueIfNeeded(node, options?.value) && matchAccessibleNameIfNeeded(node, options?.name),