Skip to content

Commit 6daaf9d

Browse files
committed
feat: implement support for Switch toBeChecked
1 parent 899de72 commit 6daaf9d

14 files changed

+85
-101
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
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

Lines changed: 45 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AccessibilityState, AccessibilityValue, StyleSheet } from 'react-native';
22
import { ReactTestInstance } from 'react-test-renderer';
33
import { getHostSiblings, getUnsafeRootElement } from './component-tree';
4-
import { getHostComponentNames, isHostText } from './host-component-names';
4+
import { getHostComponentNames, isHostSwitch, isHostText } from './host-component-names';
55
import { getTextContent } from './text-content';
66

77
type IsInaccessibleOptions = {
@@ -45,7 +45,7 @@ export function isHiddenFromAccessibility(
4545
return false;
4646
}
4747

48-
/** RTL-compatitibility alias for `isHiddenFromAccessibility` */
48+
/** RTL-compatibility alias for `isHiddenFromAccessibility` */
4949
export const isInaccessible = isHiddenFromAccessibility;
5050

5151
function isSubtreeInaccessible(element: ReactTestInstance): boolean {
@@ -140,45 +140,57 @@ export function getAccessibilityLabelledBy(element: ReactTestInstance): string |
140140
return element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy;
141141
}
142142

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;
143+
export function computeAccessibilityState(element: ReactTestInstance): AccessibilityState {
144+
const busy = computeA11yBusy(element);
145+
const checked = computeA11yChecked(element);
146+
const disabled = computeA11Disabled(element);
147+
const expanded = computeA11yExpanded(element);
148+
const selected = computeA11ySelected(element);
149+
150+
return {
151+
busy,
152+
checked,
153+
disabled,
154+
expanded,
155+
selected,
156+
};
157+
}
158+
159+
export function computeA11yBusy(element: ReactTestInstance): AccessibilityState['busy'] {
160+
const props = element.props;
161+
return props['aria-busy'] ?? props.accessibilityState?.busy;
162+
}
152163

153-
const hasAnyAccessibilityStateProps =
154-
accessibilityState != null ||
155-
ariaBusy != null ||
156-
ariaChecked != null ||
157-
ariaDisabled != null ||
158-
ariaExpanded != null ||
159-
ariaSelected != null;
164+
export function computeA11yChecked(element: ReactTestInstance): AccessibilityState['checked'] {
165+
if (isHostSwitch(element)) {
166+
return element.props.value;
167+
}
160168

161-
if (!hasAnyAccessibilityStateProps) {
169+
const role = getAccessibilityRole(element);
170+
if (role !== 'checkbox' && role !== 'radio') {
162171
return undefined;
163172
}
164173

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-
};
174+
const props = element.props;
175+
return props['aria-checked'] ?? props.accessibilityState?.checked;
172176
}
173177

174-
export function getAccessibilityCheckedState(
175-
element: ReactTestInstance,
176-
): AccessibilityState['checked'] {
177-
const { accessibilityState, 'aria-checked': ariaChecked } = element.props;
178-
return ariaChecked ?? accessibilityState?.checked;
178+
export function computeA11Disabled(element: ReactTestInstance): AccessibilityState['disabled'] {
179+
const props = element.props;
180+
return props['aria-disabled'] ?? props.accessibilityState?.disabled;
179181
}
180182

181-
export function getAccessibilityValue(element: ReactTestInstance): AccessibilityValue | undefined {
183+
export function computeA11yExpanded(element: ReactTestInstance): AccessibilityState['expanded'] {
184+
const props = element.props;
185+
return props['aria-expanded'] ?? props.accessibilityState?.expanded;
186+
}
187+
188+
export function computeA11ySelected(element: ReactTestInstance): AccessibilityState['selected'] {
189+
const props = element.props;
190+
return props['aria-selected'] ?? props.accessibilityState?.selected;
191+
}
192+
193+
export function computeAccessibilityValue(element: ReactTestInstance): AccessibilityValue {
182194
const {
183195
accessibilityValue,
184196
'aria-valuemax': ariaValueMax,
@@ -187,17 +199,6 @@ export function getAccessibilityValue(element: ReactTestInstance): Accessibility
187199
'aria-valuetext': ariaValueText,
188200
} = element.props;
189201

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-
201202
return {
202203
max: ariaValueMax ?? accessibilityValue?.max,
203204
min: ariaValueMin ?? accessibilityValue?.min,
@@ -206,33 +207,7 @@ export function getAccessibilityValue(element: ReactTestInstance): Accessibility
206207
};
207208
}
208209

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 {
210+
export function computeAccessibleName(element: ReactTestInstance): string | undefined {
236211
const label = getAccessibilityLabel(element);
237212
if (label) {
238213
return label;

src/helpers/host-component-names.tsx

Lines changed: 12 additions & 4 deletions
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 {

src/helpers/matchers/match-accessibility-state.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AccessibilityState } from 'react-native';
22
import { ReactTestInstance } from 'react-test-renderer';
3-
import { accessibilityStateKeys, getAccessibilityState } from '../accessibility';
3+
import { accessibilityStateKeys, computeAccessibilityState } from '../accessibility';
44

55
// This type is the same as AccessibilityState from `react-native` package
66
// It is re-declared here due to issues with migration from `@types/react-native` to
@@ -32,7 +32,7 @@ export function matchAccessibilityState(
3232
node: ReactTestInstance,
3333
matcher: AccessibilityStateMatcher,
3434
) {
35-
const state = getAccessibilityState(node);
35+
const state = computeAccessibilityState(node);
3636
return accessibilityStateKeys.every((key) => matchState(matcher, state, key));
3737
}
3838

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

Lines changed: 2 additions & 2 deletions
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 { computeAccessibilityValue } 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 = computeAccessibilityValue(node);
1818
return (
1919
(matcher.min === undefined || matcher.min === value?.min) &&
2020
(matcher.max === undefined || matcher.max === value?.max) &&

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ test('throws error for invalid role', () => {
160160
const unchecked = screen.getByTestId('adjustable-unchecked');
161161

162162
expect(() => expect(checked).toBeChecked()).toThrowErrorMatchingInlineSnapshot(
163-
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`,
163+
`"toBeChecked() works only on "Switch" element or accessibility elements with "checkbox" or "radio" role."`,
164164
);
165165
expect(() => expect(unchecked).not.toBeChecked()).toThrowErrorMatchingInlineSnapshot(
166-
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`,
166+
`"toBeChecked() works only on "Switch" element or accessibility elements with "checkbox" or "radio" role."`,
167167
);
168168
});
169169

@@ -172,6 +172,6 @@ test('throws error for non-accessibility element', () => {
172172

173173
const view = screen.getByTestId('test');
174174
expect(() => expect(view).toBeChecked()).toThrowErrorMatchingInlineSnapshot(
175-
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`,
175+
`"toBeChecked() works only on "Switch" element or accessibility elements with "checkbox" or "radio" role."`,
176176
);
177177
});

src/matchers/to-be-busy.tsx

Lines changed: 2 additions & 2 deletions
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 { computeA11yBusy } 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: computeA11yBusy(element) === true,
1111
message: () => {
1212
const matcher = matcherHint(`${this.isNot ? '.not' : ''}.toBeBusy`, 'element', '');
1313
return [

src/matchers/to-be-checked.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
33
import {
4-
getAccessibilityCheckedState,
54
getAccessibilityRole,
65
isAccessibilityElement,
6+
computeA11yChecked,
77
} from '../helpers/accessibility';
88
import { ErrorWithStack } from '../helpers/errors';
9+
import { isHostSwitch } from '../helpers/host-component-names';
910
import { checkHostElement, formatElement } from './utils';
1011

1112
export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstance) {
1213
checkHostElement(element, toBeChecked, this);
1314

14-
if (!hasValidAccessibilityRole(element)) {
15+
if (!isHostSwitch(element) && !isSupportedAccessibilityElement(element)) {
1516
throw new ErrorWithStack(
16-
`toBeChecked() works only on accessibility elements with "checkbox" or "radio" role.`,
17+
`toBeChecked() works only on "Switch" element or accessibility elements with "checkbox" or "radio" role.`,
1718
toBeChecked,
1819
);
1920
}
2021

2122
return {
22-
pass: getAccessibilityCheckedState(element) === true,
23+
pass: computeA11yChecked(element) === true,
2324
message: () => {
2425
const is = this.isNot ? 'is' : 'is not';
2526
return [
@@ -32,7 +33,7 @@ export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstanc
3233
};
3334
}
3435

35-
function hasValidAccessibilityRole(element: ReactTestInstance) {
36+
function isSupportedAccessibilityElement(element: ReactTestInstance) {
3637
if (!isAccessibilityElement(element)) {
3738
return false;
3839
}

src/matchers/to-be-collapsed.tsx

Lines changed: 2 additions & 2 deletions
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 { isElementCollapsed } from '../helpers/accessibility';
3+
import { computeA11yExpanded } from '../helpers/accessibility';
44
import { checkHostElement, formatElement } from './utils';
55

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

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

src/matchers/to-be-expanded.tsx

Lines changed: 2 additions & 2 deletions
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 { computeA11yExpanded } 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: computeA11yExpanded(element) === true,
1111
message: () => {
1212
const matcher = matcherHint(`${this.isNot ? '.not' : ''}.toBeExpanded`, 'element', '');
1313
return [

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
33
import {
4-
getAccessibilityCheckedState,
4+
computeA11yChecked,
55
getAccessibilityRole,
66
isAccessibilityElement,
77
} from '../helpers/accessibility';
@@ -19,7 +19,7 @@ export function toBePartiallyChecked(this: jest.MatcherContext, element: ReactTe
1919
}
2020

2121
return {
22-
pass: getAccessibilityCheckedState(element) === 'mixed',
22+
pass: computeA11yChecked(element) === 'mixed',
2323
message: () => {
2424
const is = this.isNot ? 'is' : 'is not';
2525
return [

src/matchers/to-be-selected.ts

Lines changed: 2 additions & 2 deletions
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 { computeA11ySelected } 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: computeA11ySelected(element) === true,
1111
message: () => {
1212
const is = this.isNot ? 'is' : 'is not';
1313
return [

0 commit comments

Comments
 (0)