Skip to content

Commit 22ec13d

Browse files
authoredSep 3, 2024··
refactor: accessibility handling (#1658)
* refactor: improve code structure * refactor: more tweaks
1 parent 4112abc commit 22ec13d

23 files changed

+257
-311
lines changed
 

‎.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"cSpell.words": ["Pressable", "RNTL", "Uncapitalize"]
2+
"cSpell.words": ["labelledby", "Pressable", "RNTL", "Uncapitalize", "valuenow", "valuetext"]
33
}

‎src/helpers/accessibility.ts

+47-79
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import { AccessibilityState, AccessibilityValue, StyleSheet } from 'react-native';
1+
import {
2+
AccessibilityRole,
3+
AccessibilityState,
4+
AccessibilityValue,
5+
Role,
6+
StyleSheet,
7+
} from 'react-native';
28
import { ReactTestInstance } from 'react-test-renderer';
39
import { getHostSiblings, getUnsafeRootElement } from './component-tree';
4-
import { getHostComponentNames, isHostText } from './host-component-names';
10+
import { getHostComponentNames, isHostText, isHostTextInput } from './host-component-names';
511
import { getTextContent } from './text-content';
12+
import { isTextInputEditable } from './text-input';
613

714
type IsInaccessibleOptions = {
815
cache?: WeakMap<ReactTestInstance, boolean>;
@@ -45,7 +52,7 @@ export function isHiddenFromAccessibility(
4552
return false;
4653
}
4754

48-
/** RTL-compatitibility alias for `isHiddenFromAccessibility` */
55+
/** RTL-compatibility alias for `isHiddenFromAccessibility` */
4956
export const isInaccessible = isHiddenFromAccessibility;
5057

5158
function isSubtreeInaccessible(element: ReactTestInstance): boolean {
@@ -78,7 +85,7 @@ function isSubtreeInaccessible(element: ReactTestInstance): boolean {
7885
// iOS: accessibilityViewIsModal or aria-modal
7986
// See: https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios
8087
const hostSiblings = getHostSiblings(element);
81-
if (hostSiblings.some((sibling) => getAccessibilityViewIsModal(sibling))) {
88+
if (hostSiblings.some((sibling) => computeAriaModal(sibling))) {
8289
return true;
8390
}
8491

@@ -115,7 +122,7 @@ export function isAccessibilityElement(element: ReactTestInstance | null): boole
115122
* @param element
116123
* @returns
117124
*/
118-
export function getAccessibilityRole(element: ReactTestInstance) {
125+
export function getRole(element: ReactTestInstance): Role | AccessibilityRole {
119126
const explicitRole = element.props.role ?? element.props.accessibilityRole;
120127
if (explicitRole) {
121128
return explicitRole;
@@ -128,57 +135,55 @@ export function getAccessibilityRole(element: ReactTestInstance) {
128135
return 'none';
129136
}
130137

131-
export function getAccessibilityViewIsModal(element: ReactTestInstance) {
138+
export function computeAriaModal(element: ReactTestInstance): boolean | undefined {
132139
return element.props['aria-modal'] ?? element.props.accessibilityViewIsModal;
133140
}
134141

135-
export function getAccessibilityLabel(element: ReactTestInstance): string | undefined {
142+
export function computeAriaLabel(element: ReactTestInstance): string | undefined {
136143
return element.props['aria-label'] ?? element.props.accessibilityLabel;
137144
}
138145

139-
export function getAccessibilityLabelledBy(element: ReactTestInstance): string | undefined {
146+
export function computeAriaLabelledBy(element: ReactTestInstance): string | undefined {
140147
return element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy;
141148
}
142149

143-
export function getAccessibilityState(element: ReactTestInstance): AccessibilityState | undefined {
144-
const {
145-
accessibilityState,
146-
'aria-busy': ariaBusy,
147-
'aria-checked': ariaChecked,
148-
'aria-disabled': ariaDisabled,
149-
'aria-expanded': ariaExpanded,
150-
'aria-selected': ariaSelected,
151-
} = element.props;
152-
153-
const hasAnyAccessibilityStateProps =
154-
accessibilityState != null ||
155-
ariaBusy != null ||
156-
ariaChecked != null ||
157-
ariaDisabled != null ||
158-
ariaExpanded != null ||
159-
ariaSelected != null;
150+
// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#busy-state
151+
export function computeAriaBusy({ props }: ReactTestInstance): boolean {
152+
return props['aria-busy'] ?? props.accessibilityState?.busy ?? false;
153+
}
160154

161-
if (!hasAnyAccessibilityStateProps) {
155+
// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#checked-state
156+
export function computeAriaChecked(element: ReactTestInstance): AccessibilityState['checked'] {
157+
const role = getRole(element);
158+
if (role !== 'checkbox' && role !== 'radio') {
162159
return undefined;
163160
}
164161

165-
return {
166-
busy: ariaBusy ?? accessibilityState?.busy,
167-
checked: ariaChecked ?? accessibilityState?.checked,
168-
disabled: ariaDisabled ?? accessibilityState?.disabled,
169-
expanded: ariaExpanded ?? accessibilityState?.expanded,
170-
selected: ariaSelected ?? accessibilityState?.selected,
171-
};
162+
const props = element.props;
163+
return props['aria-checked'] ?? props.accessibilityState?.checked;
164+
}
165+
166+
// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#disabled-state
167+
export function computeAriaDisabled(element: ReactTestInstance): boolean {
168+
if (isHostTextInput(element) && !isTextInputEditable(element)) {
169+
return true;
170+
}
171+
172+
const { props } = element;
173+
return props['aria-disabled'] ?? props.accessibilityState?.disabled ?? false;
174+
}
175+
176+
// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#expanded-state
177+
export function computeAriaExpanded({ props }: ReactTestInstance): boolean | undefined {
178+
return props['aria-expanded'] ?? props.accessibilityState?.expanded;
172179
}
173180

174-
export function getAccessibilityCheckedState(
175-
element: ReactTestInstance,
176-
): AccessibilityState['checked'] {
177-
const { accessibilityState, 'aria-checked': ariaChecked } = element.props;
178-
return ariaChecked ?? accessibilityState?.checked;
181+
// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#selected-state
182+
export function computeAriaSelected({ props }: ReactTestInstance): boolean {
183+
return props['aria-selected'] ?? props.accessibilityState?.selected ?? false;
179184
}
180185

181-
export function getAccessibilityValue(element: ReactTestInstance): AccessibilityValue | undefined {
186+
export function computeAriaValue(element: ReactTestInstance): AccessibilityValue {
182187
const {
183188
accessibilityValue,
184189
'aria-valuemax': ariaValueMax,
@@ -187,17 +192,6 @@ export function getAccessibilityValue(element: ReactTestInstance): Accessibility
187192
'aria-valuetext': ariaValueText,
188193
} = element.props;
189194

190-
const hasAnyAccessibilityValueProps =
191-
accessibilityValue != null ||
192-
ariaValueMax != null ||
193-
ariaValueMin != null ||
194-
ariaValueNow != null ||
195-
ariaValueText != null;
196-
197-
if (!hasAnyAccessibilityValueProps) {
198-
return undefined;
199-
}
200-
201195
return {
202196
max: ariaValueMax ?? accessibilityValue?.max,
203197
min: ariaValueMin ?? accessibilityValue?.min,
@@ -206,39 +200,13 @@ export function getAccessibilityValue(element: ReactTestInstance): Accessibility
206200
};
207201
}
208202

209-
export function isElementBusy(element: ReactTestInstance): NonNullable<AccessibilityState['busy']> {
210-
const { accessibilityState, 'aria-busy': ariaBusy } = element.props;
211-
return ariaBusy ?? accessibilityState?.busy ?? false;
212-
}
213-
214-
export function isElementCollapsed(
215-
element: ReactTestInstance,
216-
): NonNullable<AccessibilityState['expanded']> {
217-
const { accessibilityState, 'aria-expanded': ariaExpanded } = element.props;
218-
return (ariaExpanded ?? accessibilityState?.expanded) === false;
219-
}
220-
221-
export function isElementExpanded(
222-
element: ReactTestInstance,
223-
): NonNullable<AccessibilityState['expanded']> {
224-
const { accessibilityState, 'aria-expanded': ariaExpanded } = element.props;
225-
return ariaExpanded ?? accessibilityState?.expanded ?? false;
226-
}
227-
228-
export function isElementSelected(
229-
element: ReactTestInstance,
230-
): NonNullable<AccessibilityState['selected']> {
231-
const { accessibilityState, 'aria-selected': ariaSelected } = element.props;
232-
return ariaSelected ?? accessibilityState?.selected ?? false;
233-
}
234-
235-
export function getAccessibleName(element: ReactTestInstance): string | undefined {
236-
const label = getAccessibilityLabel(element);
203+
export function computeAccessibleName(element: ReactTestInstance): string | undefined {
204+
const label = computeAriaLabel(element);
237205
if (label) {
238206
return label;
239207
}
240208

241-
const labelElementId = getAccessibilityLabelledBy(element);
209+
const labelElementId = computeAriaLabelledBy(element);
242210
if (labelElementId) {
243211
const rootElement = getUnsafeRootElement(element);
244212
const labelElement = rootElement?.findByProps({ nativeID: labelElementId });

‎src/helpers/host-component-names.tsx

+12-4
Original file line numberDiff line numberDiff line change
@@ -70,31 +70,39 @@ function getByTestId(instance: ReactTestInstance, testID: string) {
7070
}
7171

7272
/**
73-
* Checks if the given element is a host Text.
73+
* Checks if the given element is a host Text element.
7474
* @param element The element to check.
7575
*/
7676
export function isHostText(element?: ReactTestInstance | null): element is HostTestInstance {
7777
return element?.type === getHostComponentNames().text;
7878
}
7979

8080
/**
81-
* Checks if the given element is a host TextInput.
81+
* Checks if the given element is a host TextInput element.
8282
* @param element The element to check.
8383
*/
8484
export function isHostTextInput(element?: ReactTestInstance | null): element is HostTestInstance {
8585
return element?.type === getHostComponentNames().textInput;
8686
}
8787

8888
/**
89-
* Checks if the given element is a host ScrollView.
89+
* Checks if the given element is a host Switch element.
90+
* @param element The element to check.
91+
*/
92+
export function isHostSwitch(element?: ReactTestInstance | null): element is HostTestInstance {
93+
return element?.type === getHostComponentNames().switch;
94+
}
95+
96+
/**
97+
* Checks if the given element is a host ScrollView element.
9098
* @param element The element to check.
9199
*/
92100
export function isHostScrollView(element?: ReactTestInstance | null): element is HostTestInstance {
93101
return element?.type === getHostComponentNames().scrollView;
94102
}
95103

96104
/**
97-
* Checks if the given element is a host Modal.
105+
* Checks if the given element is a host Modal element.
98106
* @param element The element to check.
99107
*/
100108
export function isHostModal(element?: ReactTestInstance | null): element is HostTestInstance {
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { AccessibilityState } from 'react-native';
21
import { ReactTestInstance } from 'react-test-renderer';
3-
import { accessibilityStateKeys, getAccessibilityState } from '../accessibility';
2+
import {
3+
computeAriaBusy,
4+
computeAriaChecked,
5+
computeAriaDisabled,
6+
computeAriaExpanded,
7+
computeAriaSelected,
8+
} from '../accessibility';
49

510
// This type is the same as AccessibilityState from `react-native` package
611
// It is re-declared here due to issues with migration from `@types/react-native` to
@@ -14,32 +19,25 @@ export interface AccessibilityStateMatcher {
1419
expanded?: boolean;
1520
}
1621

17-
/**
18-
* Default accessibility state values based on experiments using accessibility
19-
* inspector/screen reader on iOS and Android.
20-
*
21-
* @see https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State
22-
*/
23-
const defaultState: AccessibilityState = {
24-
disabled: false,
25-
selected: false,
26-
checked: undefined,
27-
busy: false,
28-
expanded: undefined,
29-
};
30-
3122
export function matchAccessibilityState(
3223
node: ReactTestInstance,
3324
matcher: AccessibilityStateMatcher,
3425
) {
35-
const state = getAccessibilityState(node);
36-
return accessibilityStateKeys.every((key) => matchState(matcher, state, key));
37-
}
26+
if (matcher.busy !== undefined && matcher.busy !== computeAriaBusy(node)) {
27+
return false;
28+
}
29+
if (matcher.checked !== undefined && matcher.checked !== computeAriaChecked(node)) {
30+
return false;
31+
}
32+
if (matcher.disabled !== undefined && matcher.disabled !== computeAriaDisabled(node)) {
33+
return false;
34+
}
35+
if (matcher.expanded !== undefined && matcher.expanded !== computeAriaExpanded(node)) {
36+
return false;
37+
}
38+
if (matcher.selected !== undefined && matcher.selected !== computeAriaSelected(node)) {
39+
return false;
40+
}
3841

39-
function matchState(
40-
matcher: AccessibilityStateMatcher,
41-
state: AccessibilityState | undefined,
42-
key: keyof AccessibilityState,
43-
) {
44-
return matcher[key] === undefined || matcher[key] === (state?.[key] ?? defaultState[key]);
42+
return true;
4543
}

‎src/helpers/matchers/match-accessibility-value.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ReactTestInstance } from 'react-test-renderer';
2-
import { getAccessibilityValue } from '../accessibility';
2+
import { computeAriaValue } from '../accessibility';
33
import { TextMatch } from '../../matches';
44
import { matchStringProp } from './match-string-prop';
55

@@ -14,7 +14,7 @@ export function matchAccessibilityValue(
1414
node: ReactTestInstance,
1515
matcher: AccessibilityValueMatcher,
1616
): boolean {
17-
const value = getAccessibilityValue(node);
17+
const value = computeAriaValue(node);
1818
return (
1919
(matcher.min === undefined || matcher.min === value?.min) &&
2020
(matcher.max === undefined || matcher.max === value?.max) &&

‎src/helpers/matchers/match-label-text.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { matches, TextMatch, TextMatchOptions } from '../../matches';
3-
import { getAccessibilityLabel, getAccessibilityLabelledBy } from '../accessibility';
3+
import { computeAriaLabel, computeAriaLabelledBy } from '../accessibility';
44
import { findAll } from '../find-all';
55
import { matchTextContent } from './match-text-content';
66

@@ -12,7 +12,7 @@ export function matchLabelText(
1212
) {
1313
return (
1414
matchAccessibilityLabel(element, expectedText, options) ||
15-
matchAccessibilityLabelledBy(root, getAccessibilityLabelledBy(element), expectedText, options)
15+
matchAccessibilityLabelledBy(root, computeAriaLabelledBy(element), expectedText, options)
1616
);
1717
}
1818

@@ -21,7 +21,7 @@ function matchAccessibilityLabel(
2121
extpectedLabel: TextMatch,
2222
options: TextMatchOptions,
2323
) {
24-
return matches(extpectedLabel, getAccessibilityLabel(element), options.normalizer, options.exact);
24+
return matches(extpectedLabel, computeAriaLabel(element), options.normalizer, options.exact);
2525
}
2626

2727
function matchAccessibilityLabelledBy(

‎src/matchers/__tests__/to-be-checked.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { screen } from '../../screen';
55
import '../extend-expect';
66

77
function renderViewsWithRole(role: AccessibilityRole) {
8-
return render(
8+
render(
99
<>
1010
<View
1111
testID={`${role}-checked`}

‎src/matchers/__tests__/to-be-collapsed.test.tsx

-96
This file was deleted.

‎src/matchers/__tests__/to-be-expanded.test.tsx

+92
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,95 @@ test('toBeExpanded() error messages', () => {
9494
/>"
9595
`);
9696
});
97+
98+
test('toBeCollapsed() basic case', () => {
99+
render(
100+
<>
101+
<View testID="expanded" accessibilityState={{ expanded: true }} />
102+
<View testID="expanded-aria" aria-expanded />
103+
<View testID="not-expanded" accessibilityState={{ expanded: false }} />
104+
<View testID="not-expanded-aria" aria-expanded={false} />
105+
<View testID="default" />
106+
</>,
107+
);
108+
109+
expect(screen.getByTestId('expanded')).not.toBeCollapsed();
110+
expect(screen.getByTestId('expanded-aria')).not.toBeCollapsed();
111+
expect(screen.getByTestId('not-expanded')).toBeCollapsed();
112+
expect(screen.getByTestId('not-expanded-aria')).toBeCollapsed();
113+
expect(screen.getByTestId('default')).not.toBeCollapsed();
114+
});
115+
116+
test('toBeCollapsed() error messages', () => {
117+
render(
118+
<>
119+
<View testID="expanded" accessibilityState={{ expanded: true }} />
120+
<View testID="expanded-aria" aria-expanded />
121+
<View testID="not-expanded" accessibilityState={{ expanded: false }} />
122+
<View testID="not-expanded-aria" aria-expanded={false} />
123+
<View testID="default" />
124+
</>,
125+
);
126+
127+
expect(() => expect(screen.getByTestId('expanded')).toBeCollapsed())
128+
.toThrowErrorMatchingInlineSnapshot(`
129+
"expect(element).toBeCollapsed()
130+
131+
Received element is not collapsed:
132+
<View
133+
accessibilityState={
134+
{
135+
"expanded": true,
136+
}
137+
}
138+
testID="expanded"
139+
/>"
140+
`);
141+
142+
expect(() => expect(screen.getByTestId('expanded-aria')).toBeCollapsed())
143+
.toThrowErrorMatchingInlineSnapshot(`
144+
"expect(element).toBeCollapsed()
145+
146+
Received element is not collapsed:
147+
<View
148+
aria-expanded={true}
149+
testID="expanded-aria"
150+
/>"
151+
`);
152+
153+
expect(() => expect(screen.getByTestId('not-expanded')).not.toBeCollapsed())
154+
.toThrowErrorMatchingInlineSnapshot(`
155+
"expect(element).not.toBeCollapsed()
156+
157+
Received element is collapsed:
158+
<View
159+
accessibilityState={
160+
{
161+
"expanded": false,
162+
}
163+
}
164+
testID="not-expanded"
165+
/>"
166+
`);
167+
168+
expect(() => expect(screen.getByTestId('not-expanded-aria')).not.toBeCollapsed())
169+
.toThrowErrorMatchingInlineSnapshot(`
170+
"expect(element).not.toBeCollapsed()
171+
172+
Received element is collapsed:
173+
<View
174+
aria-expanded={false}
175+
testID="not-expanded-aria"
176+
/>"
177+
`);
178+
179+
expect(() => expect(screen.getByTestId('default')).toBeCollapsed())
180+
.toThrowErrorMatchingInlineSnapshot(`
181+
"expect(element).toBeCollapsed()
182+
183+
Received element is not collapsed:
184+
<View
185+
testID="default"
186+
/>"
187+
`);
188+
});

‎src/matchers/extend-expect.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { toBeOnTheScreen } from './to-be-on-the-screen';
22
import { toBeChecked } from './to-be-checked';
3-
import { toBeCollapsed } from './to-be-collapsed';
43
import { toBeDisabled, toBeEnabled } from './to-be-disabled';
54
import { toBeBusy } from './to-be-busy';
65
import { toBeEmptyElement } from './to-be-empty-element';
7-
import { toBeExpanded } from './to-be-expanded';
6+
import { toBeExpanded, toBeCollapsed } from './to-be-expanded';
87
import { toBePartiallyChecked } from './to-be-partially-checked';
98
import { toBeSelected } from './to-be-selected';
109
import { toBeVisible } from './to-be-visible';

‎src/matchers/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
export { toBeBusy } from './to-be-busy';
22
export { toBeChecked } from './to-be-checked';
3-
export { toBeCollapsed } from './to-be-collapsed';
43
export { toBeDisabled, toBeEnabled } from './to-be-disabled';
54
export { toBeEmptyElement } from './to-be-empty-element';
6-
export { toBeExpanded } from './to-be-expanded';
5+
export { toBeExpanded, toBeCollapsed } from './to-be-expanded';
76
export { toBeOnTheScreen } from './to-be-on-the-screen';
87
export { toBePartiallyChecked } from './to-be-partially-checked';
98
export { toBeSelected } from './to-be-selected';

‎src/matchers/to-be-busy.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3-
import { isElementBusy } from '../helpers/accessibility';
3+
import { computeAriaBusy } from '../helpers/accessibility';
44
import { checkHostElement, formatElement } from './utils';
55

66
export function toBeBusy(this: jest.MatcherContext, element: ReactTestInstance) {
77
checkHostElement(element, toBeBusy, this);
88

99
return {
10-
pass: isElementBusy(element),
10+
pass: computeAriaBusy(element),
1111
message: () => {
1212
const matcher = matcherHint(`${this.isNot ? '.not' : ''}.toBeBusy`, 'element', '');
1313
return [

‎src/matchers/to-be-checked.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3-
import {
4-
getAccessibilityCheckedState,
5-
getAccessibilityRole,
6-
isAccessibilityElement,
7-
} from '../helpers/accessibility';
3+
import { computeAriaChecked, getRole, isAccessibilityElement } from '../helpers/accessibility';
84
import { ErrorWithStack } from '../helpers/errors';
95
import { checkHostElement, formatElement } from './utils';
106

@@ -19,7 +15,7 @@ export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstanc
1915
}
2016

2117
return {
22-
pass: getAccessibilityCheckedState(element) === true,
18+
pass: computeAriaChecked(element) === true,
2319
message: () => {
2420
const is = this.isNot ? 'is' : 'is not';
2521
return [
@@ -37,6 +33,6 @@ function hasValidAccessibilityRole(element: ReactTestInstance) {
3733
return false;
3834
}
3935

40-
const role = getAccessibilityRole(element);
36+
const role = getRole(element);
4137
return role === 'checkbox' || role === 'radio';
4238
}

‎src/matchers/to-be-collapsed.tsx

-21
This file was deleted.

‎src/matchers/to-be-disabled.tsx

+4-14
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3-
import { isHostTextInput } from '../helpers/host-component-names';
4-
import { isTextInputEditable } from '../helpers/text-input';
3+
import { computeAriaDisabled } from '../helpers/accessibility';
54
import { getHostParent } from '../helpers/component-tree';
65
import { checkHostElement, formatElement } from './utils';
76

87
export function toBeDisabled(this: jest.MatcherContext, element: ReactTestInstance) {
98
checkHostElement(element, toBeDisabled, this);
109

11-
const isDisabled = isElementDisabled(element) || isAncestorDisabled(element);
10+
const isDisabled = computeAriaDisabled(element) || isAncestorDisabled(element);
1211

1312
return {
1413
pass: isDisabled,
@@ -27,7 +26,7 @@ export function toBeDisabled(this: jest.MatcherContext, element: ReactTestInstan
2726
export function toBeEnabled(this: jest.MatcherContext, element: ReactTestInstance) {
2827
checkHostElement(element, toBeEnabled, this);
2928

30-
const isEnabled = !isElementDisabled(element) && !isAncestorDisabled(element);
29+
const isEnabled = !computeAriaDisabled(element) && !isAncestorDisabled(element);
3130

3231
return {
3332
pass: isEnabled,
@@ -43,20 +42,11 @@ export function toBeEnabled(this: jest.MatcherContext, element: ReactTestInstanc
4342
};
4443
}
4544

46-
function isElementDisabled(element: ReactTestInstance) {
47-
if (isHostTextInput(element) && !isTextInputEditable(element)) {
48-
return true;
49-
}
50-
51-
const { accessibilityState, 'aria-disabled': ariaDisabled } = element.props;
52-
return ariaDisabled ?? accessibilityState?.disabled ?? false;
53-
}
54-
5545
function isAncestorDisabled(element: ReactTestInstance): boolean {
5646
const parent = getHostParent(element);
5747
if (parent == null) {
5848
return false;
5949
}
6050

61-
return isElementDisabled(parent) || isAncestorDisabled(parent);
51+
return computeAriaDisabled(parent) || isAncestorDisabled(parent);
6252
}

‎src/matchers/to-be-expanded.tsx

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3-
import { isElementExpanded } from '../helpers/accessibility';
3+
import { computeAriaExpanded } from '../helpers/accessibility';
44
import { checkHostElement, formatElement } from './utils';
55

66
export function toBeExpanded(this: jest.MatcherContext, element: ReactTestInstance) {
77
checkHostElement(element, toBeExpanded, this);
88

99
return {
10-
pass: isElementExpanded(element),
10+
pass: computeAriaExpanded(element) === true,
1111
message: () => {
1212
const matcher = matcherHint(`${this.isNot ? '.not' : ''}.toBeExpanded`, 'element', '');
1313
return [
@@ -19,3 +19,20 @@ export function toBeExpanded(this: jest.MatcherContext, element: ReactTestInstan
1919
},
2020
};
2121
}
22+
23+
export function toBeCollapsed(this: jest.MatcherContext, element: ReactTestInstance) {
24+
checkHostElement(element, toBeCollapsed, this);
25+
26+
return {
27+
pass: computeAriaExpanded(element) === false,
28+
message: () => {
29+
const matcher = matcherHint(`${this.isNot ? '.not' : ''}.toBeCollapsed`, 'element', '');
30+
return [
31+
matcher,
32+
'',
33+
`Received element is ${this.isNot ? '' : 'not '}collapsed:`,
34+
formatElement(element),
35+
].join('\n');
36+
},
37+
};
38+
}

‎src/matchers/to-be-partially-checked.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3-
import {
4-
getAccessibilityCheckedState,
5-
getAccessibilityRole,
6-
isAccessibilityElement,
7-
} from '../helpers/accessibility';
3+
import { computeAriaChecked, getRole, isAccessibilityElement } from '../helpers/accessibility';
84
import { ErrorWithStack } from '../helpers/errors';
95
import { checkHostElement, formatElement } from './utils';
106

@@ -19,7 +15,7 @@ export function toBePartiallyChecked(this: jest.MatcherContext, element: ReactTe
1915
}
2016

2117
return {
22-
pass: getAccessibilityCheckedState(element) === 'mixed',
18+
pass: computeAriaChecked(element) === 'mixed',
2319
message: () => {
2420
const is = this.isNot ? 'is' : 'is not';
2521
return [
@@ -33,6 +29,6 @@ export function toBePartiallyChecked(this: jest.MatcherContext, element: ReactTe
3329
}
3430

3531
function hasValidAccessibilityRole(element: ReactTestInstance) {
36-
const role = getAccessibilityRole(element);
32+
const role = getRole(element);
3733
return isAccessibilityElement(element) && role === 'checkbox';
3834
}

‎src/matchers/to-be-selected.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3-
import { isElementSelected } from '../helpers/accessibility';
3+
import { computeAriaSelected } from '../helpers/accessibility';
44
import { checkHostElement, formatElement } from './utils';
55

66
export function toBeSelected(this: jest.MatcherContext, element: ReactTestInstance) {
77
checkHostElement(element, toBeSelected, this);
88

99
return {
10-
pass: isElementSelected(element),
10+
pass: computeAriaSelected(element),
1111
message: () => {
1212
const is = this.isNot ? 'is' : 'is not';
1313
return [

‎src/matchers/to-have-accessibility-value.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint, stringify } from 'jest-matcher-utils';
3-
import { getAccessibilityValue } from '../helpers/accessibility';
3+
import { computeAriaValue } from '../helpers/accessibility';
44
import {
55
AccessibilityValueMatcher,
66
matchAccessibilityValue,
@@ -15,7 +15,7 @@ export function toHaveAccessibilityValue(
1515
) {
1616
checkHostElement(element, toHaveAccessibilityValue, this);
1717

18-
const receivedValue = getAccessibilityValue(element);
18+
const receivedValue = computeAriaValue(element);
1919

2020
return {
2121
pass: matchAccessibilityValue(element, expectedValue),

‎src/matchers/to-have-accessible-name.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3-
import { getAccessibleName } from '../helpers/accessibility';
3+
import { computeAccessibleName } from '../helpers/accessibility';
44
import { TextMatch, TextMatchOptions, matches } from '../matches';
55
import { checkHostElement, formatMessage } from './utils';
66

@@ -12,7 +12,7 @@ export function toHaveAccessibleName(
1212
) {
1313
checkHostElement(element, toHaveAccessibleName, this);
1414

15-
const receivedName = getAccessibleName(element);
15+
const receivedName = computeAccessibleName(element);
1616
const missingExpectedValue = arguments.length === 1;
1717

1818
let pass = false;

‎src/queries/__tests__/accessibility-state.test.tsx

+15-15
Original file line numberDiff line numberDiff line change
@@ -88,31 +88,31 @@ test('getAllByA11yState, queryAllByA11yState, findAllByA11yState', async () => {
8888

8989
describe('checked state matching', () => {
9090
it('handles true', () => {
91-
render(<View accessibilityState={{ checked: true }} />);
91+
render(<View role="checkbox" accessibilityState={{ checked: true }} />);
9292

9393
expect(screen.getByA11yState({ checked: true })).toBeTruthy();
9494
expect(screen.queryByA11yState({ checked: 'mixed' })).toBeFalsy();
9595
expect(screen.queryByA11yState({ checked: false })).toBeFalsy();
9696
});
9797

9898
it('handles mixed', () => {
99-
render(<View accessibilityState={{ checked: 'mixed' }} />);
99+
render(<View role="checkbox" accessibilityState={{ checked: 'mixed' }} />);
100100

101101
expect(screen.getByA11yState({ checked: 'mixed' })).toBeTruthy();
102102
expect(screen.queryByA11yState({ checked: true })).toBeFalsy();
103103
expect(screen.queryByA11yState({ checked: false })).toBeFalsy();
104104
});
105105

106106
it('handles false', () => {
107-
render(<View accessibilityState={{ checked: false }} />);
107+
render(<View role="checkbox" accessibilityState={{ checked: false }} />);
108108

109109
expect(screen.getByA11yState({ checked: false })).toBeTruthy();
110110
expect(screen.queryByA11yState({ checked: true })).toBeFalsy();
111111
expect(screen.queryByA11yState({ checked: 'mixed' })).toBeFalsy();
112112
});
113113

114-
it('handles default', () => {
115-
render(<View accessibilityState={{}} />);
114+
it('handles default', () => {
115+
render(<View role="checkbox" accessibilityState={{}} />);
116116

117117
expect(screen.queryByA11yState({ checked: false })).toBeFalsy();
118118
expect(screen.queryByA11yState({ checked: true })).toBeFalsy();
@@ -135,7 +135,7 @@ describe('expanded state matching', () => {
135135
expect(screen.queryByA11yState({ expanded: true })).toBeFalsy();
136136
});
137137

138-
it('handles default', () => {
138+
it('handles default', () => {
139139
render(<View accessibilityState={{}} />);
140140

141141
expect(screen.queryByA11yState({ expanded: false })).toBeFalsy();
@@ -158,7 +158,7 @@ describe('disabled state matching', () => {
158158
expect(screen.queryByA11yState({ disabled: true })).toBeFalsy();
159159
});
160160

161-
it('handles default', () => {
161+
it('handles default', () => {
162162
render(<View accessibilityState={{}} />);
163163

164164
expect(screen.getByA11yState({ disabled: false })).toBeTruthy();
@@ -181,7 +181,7 @@ describe('busy state matching', () => {
181181
expect(screen.queryByA11yState({ busy: true })).toBeFalsy();
182182
});
183183

184-
it('handles default', () => {
184+
it('handles default', () => {
185185
render(<View accessibilityState={{}} />);
186186

187187
expect(screen.getByA11yState({ busy: false })).toBeTruthy();
@@ -204,7 +204,7 @@ describe('selected state matching', () => {
204204
expect(screen.queryByA11yState({ selected: true })).toBeFalsy();
205205
});
206206

207-
it('handles default', () => {
207+
it('handles default', () => {
208208
render(<View accessibilityState={{}} />);
209209

210210
expect(screen.getByA11yState({ selected: false })).toBeTruthy();
@@ -463,28 +463,28 @@ describe('aria-selected prop', () => {
463463

464464
describe('aria-checked prop', () => {
465465
test('supports aria-checked={true} prop', () => {
466-
render(<View accessible accessibilityRole="button" aria-checked={true} />);
466+
render(<View accessible role="checkbox" aria-checked={true} />);
467467
expect(screen.getByAccessibilityState({ checked: true })).toBeTruthy();
468468
expect(screen.queryByAccessibilityState({ checked: false })).toBeNull();
469469
expect(screen.queryByAccessibilityState({ checked: 'mixed' })).toBeNull();
470470
});
471471

472472
test('supports aria-checked={false} prop', () => {
473-
render(<View accessible accessibilityRole="button" aria-checked={false} />);
473+
render(<View accessible role="checkbox" aria-checked={false} />);
474474
expect(screen.getByAccessibilityState({ checked: false })).toBeTruthy();
475475
expect(screen.queryByAccessibilityState({ checked: true })).toBeNull();
476476
expect(screen.queryByAccessibilityState({ checked: 'mixed' })).toBeNull();
477477
});
478478

479-
test('supports aria-checked="mixed prop', () => {
480-
render(<View accessible accessibilityRole="button" aria-checked="mixed" />);
479+
test('supports aria-checked="mixed" prop', () => {
480+
render(<View accessible accessibilityRole="checkbox" aria-checked="mixed" />);
481481
expect(screen.getByAccessibilityState({ checked: 'mixed' })).toBeTruthy();
482482
expect(screen.queryByAccessibilityState({ checked: true })).toBeNull();
483483
expect(screen.queryByAccessibilityState({ checked: false })).toBeNull();
484484
});
485485

486-
test('supports default aria-selected prop', () => {
487-
render(<View accessible accessibilityRole="button" />);
486+
test('supports default aria-checked prop', () => {
487+
render(<View accessible accessibilityRole="checkbox" />);
488488
expect(screen.getByAccessibilityState({})).toBeTruthy();
489489
expect(screen.queryByAccessibilityState({ checked: true })).toBeNull();
490490
expect(screen.queryByAccessibilityState({ checked: false })).toBeNull();

‎src/queries/__tests__/role.test.tsx

+20-20
Original file line numberDiff line numberDiff line change
@@ -509,32 +509,32 @@ describe('supports accessibility states', () => {
509509
});
510510

511511
test('supports aria-checked={true} prop', () => {
512-
render(<View accessible accessibilityRole="button" aria-checked={true} />);
513-
expect(screen.getByRole('button', { checked: true })).toBeTruthy();
514-
expect(screen.queryByRole('button', { checked: false })).toBeNull();
515-
expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull();
512+
render(<View accessible role="checkbox" aria-checked={true} />);
513+
expect(screen.getByRole('checkbox', { checked: true })).toBeTruthy();
514+
expect(screen.queryByRole('checkbox', { checked: false })).toBeNull();
515+
expect(screen.queryByRole('checkbox', { checked: 'mixed' })).toBeNull();
516516
});
517517

518518
test('supports aria-checked={false} prop', () => {
519-
render(<View accessible accessibilityRole="button" aria-checked={false} />);
520-
expect(screen.getByRole('button', { checked: false })).toBeTruthy();
521-
expect(screen.queryByRole('button', { checked: true })).toBeNull();
522-
expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull();
519+
render(<View accessible role="checkbox" aria-checked={false} />);
520+
expect(screen.getByRole('checkbox', { checked: false })).toBeTruthy();
521+
expect(screen.queryByRole('checkbox', { checked: true })).toBeNull();
522+
expect(screen.queryByRole('checkbox', { checked: 'mixed' })).toBeNull();
523523
});
524524

525-
test('supports aria-checked="mixed prop', () => {
526-
render(<View accessible accessibilityRole="button" aria-checked="mixed" />);
527-
expect(screen.getByRole('button', { checked: 'mixed' })).toBeTruthy();
528-
expect(screen.queryByRole('button', { checked: true })).toBeNull();
529-
expect(screen.queryByRole('button', { checked: false })).toBeNull();
525+
test('supports aria-checked="mixed" prop', () => {
526+
render(<View accessible role="checkbox" aria-checked="mixed" />);
527+
expect(screen.getByRole('checkbox', { checked: 'mixed' })).toBeTruthy();
528+
expect(screen.queryByRole('checkbox', { checked: true })).toBeNull();
529+
expect(screen.queryByRole('checkbox', { checked: false })).toBeNull();
530530
});
531531

532532
test('supports default aria-selected prop', () => {
533-
render(<View accessible accessibilityRole="button" />);
534-
expect(screen.getByRole('button')).toBeTruthy();
535-
expect(screen.queryByRole('button', { checked: true })).toBeNull();
536-
expect(screen.queryByRole('button', { checked: false })).toBeNull();
537-
expect(screen.queryByRole('button', { checked: 'mixed' })).toBeNull();
533+
render(<View accessible role="checkbox" />);
534+
expect(screen.getByRole('checkbox')).toBeTruthy();
535+
expect(screen.queryByRole('checkbox', { checked: true })).toBeNull();
536+
expect(screen.queryByRole('checkbox', { checked: false })).toBeNull();
537+
expect(screen.queryByRole('checkbox', { checked: 'mixed' })).toBeNull();
538538
});
539539
});
540540

@@ -728,7 +728,7 @@ describe('supports accessibility states', () => {
728728
test('matches an element combining all the options', () => {
729729
render(
730730
<TouchableOpacity
731-
accessibilityRole="button"
731+
accessibilityRole="checkbox"
732732
accessibilityState={{
733733
disabled: true,
734734
selected: true,
@@ -742,7 +742,7 @@ describe('supports accessibility states', () => {
742742
);
743743

744744
expect(
745-
screen.getByRole('button', {
745+
screen.getByRole('checkbox', {
746746
name: 'Save',
747747
disabled: true,
748748
selected: true,

‎src/queries/role.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { AccessibilityRole, Role } from 'react-native';
33
import {
44
accessibilityStateKeys,
55
accessibilityValueKeys,
6-
getAccessibilityRole,
6+
getRole,
77
isAccessibilityElement,
88
} from '../helpers/accessibility';
99
import { findAll } from '../helpers/find-all';
@@ -65,7 +65,7 @@ const queryAllByRole = (
6565
(node) =>
6666
// run the cheapest checks first, and early exit to avoid unneeded computations
6767
isAccessibilityElement(node) &&
68-
matchStringProp(getAccessibilityRole(node), role) &&
68+
matchStringProp(getRole(node), role) &&
6969
matchAccessibleStateIfNeeded(node, options) &&
7070
matchAccessibilityValueIfNeeded(node, options?.value) &&
7171
matchAccessibleNameIfNeeded(node, options?.name),

0 commit comments

Comments
 (0)
Please sign in to comment.