refactor: accessibility handling (#1658)

* refactor: improve code structure

* refactor: more tweaks
This commit is contained in:
Maciej Jastrzebski
2024-09-03 09:41:47 +02:00
committed by GitHub
parent 4112abcf0a
commit 22ec13d0e5
23 changed files with 257 additions and 311 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
"cSpell.words": ["Pressable", "RNTL", "Uncapitalize"]
"cSpell.words": ["labelledby", "Pressable", "RNTL", "Uncapitalize", "valuenow", "valuetext"]
}
+47 -79
View File
@@ -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<ReactTestInstance, boolean>;
@@ -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;
// 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;
}
const hasAnyAccessibilityStateProps =
accessibilityState != null ||
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null;
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;
}
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#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;
}
export function getAccessibilityValue(element: ReactTestInstance): AccessibilityValue | undefined {
// 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;
}
// 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 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<AccessibilityState['busy']> {
const { accessibilityState, 'aria-busy': ariaBusy } = element.props;
return ariaBusy ?? accessibilityState?.busy ?? false;
}
export function isElementCollapsed(
element: ReactTestInstance,
): NonNullable<AccessibilityState['expanded']> {
const { accessibilityState, 'aria-expanded': ariaExpanded } = element.props;
return (ariaExpanded ?? accessibilityState?.expanded) === false;
}
export function isElementExpanded(
element: ReactTestInstance,
): NonNullable<AccessibilityState['expanded']> {
const { accessibilityState, 'aria-expanded': ariaExpanded } = element.props;
return ariaExpanded ?? accessibilityState?.expanded ?? false;
}
export function isElementSelected(
element: ReactTestInstance,
): NonNullable<AccessibilityState['selected']> {
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 });
+12 -4
View File
@@ -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 {
@@ -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;
}
@@ -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) &&
+3 -3
View File
@@ -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(
@@ -5,7 +5,7 @@ import { screen } from '../../screen';
import '../extend-expect';
function renderViewsWithRole(role: AccessibilityRole) {
return render(
render(
<>
<View
testID={`${role}-checked`}
@@ -1,96 +0,0 @@
import * as React from 'react';
import { View } from 'react-native';
import { render, screen } from '../..';
import '../extend-expect';
test('toBeCollapsed() basic case', () => {
render(
<>
<View testID="expanded" accessibilityState={{ expanded: true }} />
<View testID="expanded-aria" aria-expanded />
<View testID="not-expanded" accessibilityState={{ expanded: false }} />
<View testID="not-expanded-aria" aria-expanded={false} />
<View testID="default" />
</>,
);
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(
<>
<View testID="expanded" accessibilityState={{ expanded: true }} />
<View testID="expanded-aria" aria-expanded />
<View testID="not-expanded" accessibilityState={{ expanded: false }} />
<View testID="not-expanded-aria" aria-expanded={false} />
<View testID="default" />
</>,
);
expect(() => expect(screen.getByTestId('expanded')).toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeCollapsed()
Received element is not collapsed:
<View
accessibilityState={
{
"expanded": true,
}
}
testID="expanded"
/>"
`);
expect(() => expect(screen.getByTestId('expanded-aria')).toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeCollapsed()
Received element is not collapsed:
<View
aria-expanded={true}
testID="expanded-aria"
/>"
`);
expect(() => expect(screen.getByTestId('not-expanded')).not.toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBeCollapsed()
Received element is collapsed:
<View
accessibilityState={
{
"expanded": false,
}
}
testID="not-expanded"
/>"
`);
expect(() => expect(screen.getByTestId('not-expanded-aria')).not.toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBeCollapsed()
Received element is collapsed:
<View
aria-expanded={false}
testID="not-expanded-aria"
/>"
`);
expect(() => expect(screen.getByTestId('default')).toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeCollapsed()
Received element is not collapsed:
<View
testID="default"
/>"
`);
});
@@ -94,3 +94,95 @@ test('toBeExpanded() error messages', () => {
/>"
`);
});
test('toBeCollapsed() basic case', () => {
render(
<>
<View testID="expanded" accessibilityState={{ expanded: true }} />
<View testID="expanded-aria" aria-expanded />
<View testID="not-expanded" accessibilityState={{ expanded: false }} />
<View testID="not-expanded-aria" aria-expanded={false} />
<View testID="default" />
</>,
);
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(
<>
<View testID="expanded" accessibilityState={{ expanded: true }} />
<View testID="expanded-aria" aria-expanded />
<View testID="not-expanded" accessibilityState={{ expanded: false }} />
<View testID="not-expanded-aria" aria-expanded={false} />
<View testID="default" />
</>,
);
expect(() => expect(screen.getByTestId('expanded')).toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeCollapsed()
Received element is not collapsed:
<View
accessibilityState={
{
"expanded": true,
}
}
testID="expanded"
/>"
`);
expect(() => expect(screen.getByTestId('expanded-aria')).toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeCollapsed()
Received element is not collapsed:
<View
aria-expanded={true}
testID="expanded-aria"
/>"
`);
expect(() => expect(screen.getByTestId('not-expanded')).not.toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBeCollapsed()
Received element is collapsed:
<View
accessibilityState={
{
"expanded": false,
}
}
testID="not-expanded"
/>"
`);
expect(() => expect(screen.getByTestId('not-expanded-aria')).not.toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBeCollapsed()
Received element is collapsed:
<View
aria-expanded={false}
testID="not-expanded-aria"
/>"
`);
expect(() => expect(screen.getByTestId('default')).toBeCollapsed())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeCollapsed()
Received element is not collapsed:
<View
testID="default"
/>"
`);
});
+1 -2
View File
@@ -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';
+1 -2
View File
@@ -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';
+2 -2
View File
@@ -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 [
+3 -7
View File
@@ -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';
}
-21
View File
@@ -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');
},
};
}
+4 -14
View File
@@ -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);
}
+19 -2
View File
@@ -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');
},
};
}
+3 -7
View File
@@ -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';
}
+2 -2
View File
@@ -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 [
+2 -2
View File
@@ -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),
+2 -2
View File
@@ -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;
@@ -88,7 +88,7 @@ test('getAllByA11yState, queryAllByA11yState, findAllByA11yState', async () => {
describe('checked state matching', () => {
it('handles true', () => {
render(<View accessibilityState={{ checked: true }} />);
render(<View role="checkbox" accessibilityState={{ checked: true }} />);
expect(screen.getByA11yState({ checked: true })).toBeTruthy();
expect(screen.queryByA11yState({ checked: 'mixed' })).toBeFalsy();
@@ -96,7 +96,7 @@ describe('checked state matching', () => {
});
it('handles mixed', () => {
render(<View accessibilityState={{ checked: 'mixed' }} />);
render(<View role="checkbox" accessibilityState={{ checked: 'mixed' }} />);
expect(screen.getByA11yState({ checked: 'mixed' })).toBeTruthy();
expect(screen.queryByA11yState({ checked: true })).toBeFalsy();
@@ -104,15 +104,15 @@ describe('checked state matching', () => {
});
it('handles false', () => {
render(<View accessibilityState={{ checked: false }} />);
render(<View role="checkbox" accessibilityState={{ checked: false }} />);
expect(screen.getByA11yState({ checked: false })).toBeTruthy();
expect(screen.queryByA11yState({ checked: true })).toBeFalsy();
expect(screen.queryByA11yState({ checked: 'mixed' })).toBeFalsy();
});
it('handles default', () => {
render(<View accessibilityState={{}} />);
it('handles default', () => {
render(<View role="checkbox" accessibilityState={{}} />);
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(<View accessibilityState={{}} />);
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(<View accessibilityState={{}} />);
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(<View accessibilityState={{}} />);
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(<View accessibilityState={{}} />);
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(<View accessible accessibilityRole="button" aria-checked={true} />);
render(<View accessible role="checkbox" aria-checked={true} />);
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(<View accessible accessibilityRole="button" aria-checked={false} />);
render(<View accessible role="checkbox" aria-checked={false} />);
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(<View accessible accessibilityRole="button" aria-checked="mixed" />);
test('supports aria-checked="mixed" prop', () => {
render(<View accessible accessibilityRole="checkbox" aria-checked="mixed" />);
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(<View accessible accessibilityRole="button" />);
test('supports default aria-checked prop', () => {
render(<View accessible accessibilityRole="checkbox" />);
expect(screen.getByAccessibilityState({})).toBeTruthy();
expect(screen.queryByAccessibilityState({ checked: true })).toBeNull();
expect(screen.queryByAccessibilityState({ checked: false })).toBeNull();
+20 -20
View File
@@ -509,32 +509,32 @@ describe('supports accessibility states', () => {
});
test('supports aria-checked={true} prop', () => {
render(<View accessible accessibilityRole="button" aria-checked={true} />);
expect(screen.getByRole('button', { checked: true })).toBeTruthy();
expect(screen.queryByRole('button', { checked: false })).toBeNull();
expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull();
render(<View accessible role="checkbox" aria-checked={true} />);
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(<View accessible accessibilityRole="button" aria-checked={false} />);
expect(screen.getByRole('button', { checked: false })).toBeTruthy();
expect(screen.queryByRole('button', { checked: true })).toBeNull();
expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull();
render(<View accessible role="checkbox" aria-checked={false} />);
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(<View accessible accessibilityRole="button" aria-checked="mixed" />);
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(<View accessible role="checkbox" aria-checked="mixed" />);
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(<View accessible accessibilityRole="button" />);
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(<View accessible role="checkbox" />);
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(
<TouchableOpacity
accessibilityRole="button"
accessibilityRole="checkbox"
accessibilityState={{
disabled: true,
selected: true,
@@ -742,7 +742,7 @@ describe('supports accessibility states', () => {
);
expect(
screen.getByRole('button', {
screen.getByRole('checkbox', {
name: 'Save',
disabled: true,
selected: true,
+2 -2
View File
@@ -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),