Skip to content

Commit 856332c

Browse files
feat: support Switch checked state (#1657)
1 parent bf7ea4c commit 856332c

File tree

7 files changed

+155
-20
lines changed

7 files changed

+155
-20
lines changed

src/helpers/accessibility.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import {
77
} from 'react-native';
88
import { ReactTestInstance } from 'react-test-renderer';
99
import { getHostSiblings, getUnsafeRootElement } from './component-tree';
10-
import { getHostComponentNames, isHostText, isHostTextInput } from './host-component-names';
10+
import {
11+
getHostComponentNames,
12+
isHostSwitch,
13+
isHostText,
14+
isHostTextInput,
15+
} from './host-component-names';
1116
import { getTextContent } from './text-content';
1217
import { isTextInputEditable } from './text-input';
1318

@@ -154,12 +159,17 @@ export function computeAriaBusy({ props }: ReactTestInstance): boolean {
154159

155160
// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#checked-state
156161
export function computeAriaChecked(element: ReactTestInstance): AccessibilityState['checked'] {
162+
const { props } = element;
163+
164+
if (isHostSwitch(element)) {
165+
return props.value;
166+
}
167+
157168
const role = getRole(element);
158-
if (role !== 'checkbox' && role !== 'radio') {
169+
if (!rolesSupportingCheckedState[role]) {
159170
return undefined;
160171
}
161172

162-
const props = element.props;
163173
return props['aria-checked'] ?? props.accessibilityState?.checked;
164174
}
165175

@@ -217,3 +227,11 @@ export function computeAccessibleName(element: ReactTestInstance): string | unde
217227

218228
return getTextContent(element);
219229
}
230+
231+
type RoleSupportMap = Partial<Record<Role | AccessibilityRole, true>>;
232+
233+
export const rolesSupportingCheckedState: RoleSupportMap = {
234+
checkbox: true,
235+
radio: true,
236+
switch: true,
237+
};

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

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { type AccessibilityRole, View } from 'react-native';
2+
import { type AccessibilityRole, Switch, View } from 'react-native';
33
import render from '../../render';
44
import { screen } from '../../screen';
55
import '../extend-expect';
@@ -30,7 +30,56 @@ function renderViewsWithRole(role: AccessibilityRole) {
3030
);
3131
}
3232

33-
test('toBeCheck() with checkbox role', () => {
33+
test('toBeCheck() with Switch', () => {
34+
render(
35+
<>
36+
<Switch testID="checked" value={true} />
37+
<Switch testID="unchecked" value={false} />
38+
<Switch testID="default" />
39+
</>,
40+
);
41+
42+
const checked = screen.getByTestId('checked');
43+
const unchecked = screen.getByTestId('unchecked');
44+
const defaultView = screen.getByTestId('default');
45+
46+
expect(checked).toBeChecked();
47+
expect(unchecked).not.toBeChecked();
48+
expect(defaultView).not.toBeChecked();
49+
50+
expect(() => expect(checked).not.toBeChecked()).toThrowErrorMatchingInlineSnapshot(`
51+
"expect(element).not.toBeChecked()
52+
53+
Received element is checked:
54+
<RCTSwitch
55+
accessibilityRole="switch"
56+
testID="checked"
57+
value={true}
58+
/>"
59+
`);
60+
expect(() => expect(unchecked).toBeChecked()).toThrowErrorMatchingInlineSnapshot(`
61+
"expect(element).toBeChecked()
62+
63+
Received element is not checked:
64+
<RCTSwitch
65+
accessibilityRole="switch"
66+
testID="unchecked"
67+
value={false}
68+
/>"
69+
`);
70+
expect(() => expect(defaultView).toBeChecked()).toThrowErrorMatchingInlineSnapshot(`
71+
"expect(element).toBeChecked()
72+
73+
Received element is not checked:
74+
<RCTSwitch
75+
accessibilityRole="switch"
76+
testID="default"
77+
value={false}
78+
/>"
79+
`);
80+
});
81+
82+
test('toBeCheck() with "checkbox" role', () => {
3483
renderViewsWithRole('checkbox');
3584

3685
const checked = screen.getByTestId('checkbox-checked');
@@ -100,7 +149,7 @@ test('toBeCheck() with checkbox role', () => {
100149
`);
101150
});
102151

103-
test('toBeCheck() with radio role', () => {
152+
test('toBeCheck() with "radio" role', () => {
104153
renderViewsWithRole('radio');
105154

106155
const checked = screen.getByTestId('radio-checked');
@@ -153,17 +202,70 @@ test('toBeCheck() with radio role', () => {
153202
`);
154203
});
155204

205+
test('toBeCheck() with "switch" role', () => {
206+
renderViewsWithRole('switch');
207+
208+
const checked = screen.getByTestId('switch-checked');
209+
const unchecked = screen.getByTestId('switch-unchecked');
210+
const defaultView = screen.getByTestId('switch-default');
211+
212+
expect(checked).toBeChecked();
213+
expect(unchecked).not.toBeChecked();
214+
expect(defaultView).not.toBeChecked();
215+
216+
expect(() => expect(checked).not.toBeChecked()).toThrowErrorMatchingInlineSnapshot(`
217+
"expect(element).not.toBeChecked()
218+
219+
Received element is checked:
220+
<View
221+
accessibilityRole="switch"
222+
accessibilityState={
223+
{
224+
"checked": true,
225+
}
226+
}
227+
accessible={true}
228+
testID="switch-checked"
229+
/>"
230+
`);
231+
expect(() => expect(unchecked).toBeChecked()).toThrowErrorMatchingInlineSnapshot(`
232+
"expect(element).toBeChecked()
233+
234+
Received element is not checked:
235+
<View
236+
accessibilityRole="switch"
237+
accessibilityState={
238+
{
239+
"checked": false,
240+
}
241+
}
242+
accessible={true}
243+
testID="switch-unchecked"
244+
/>"
245+
`);
246+
expect(() => expect(defaultView).toBeChecked()).toThrowErrorMatchingInlineSnapshot(`
247+
"expect(element).toBeChecked()
248+
249+
Received element is not checked:
250+
<View
251+
accessibilityRole="switch"
252+
accessible={true}
253+
testID="switch-default"
254+
/>"
255+
`);
256+
});
257+
156258
test('throws error for invalid role', () => {
157259
renderViewsWithRole('adjustable');
158260

159261
const checked = screen.getByTestId('adjustable-checked');
160262
const unchecked = screen.getByTestId('adjustable-unchecked');
161263

162264
expect(() => expect(checked).toBeChecked()).toThrowErrorMatchingInlineSnapshot(
163-
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`,
265+
`"toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role."`,
164266
);
165267
expect(() => expect(unchecked).not.toBeChecked()).toThrowErrorMatchingInlineSnapshot(
166-
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`,
268+
`"toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role."`,
167269
);
168270
});
169271

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

173275
const view = screen.getByTestId('test');
174276
expect(() => expect(view).toBeChecked()).toThrowErrorMatchingInlineSnapshot(
175-
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`,
277+
`"toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role."`,
176278
);
177279
});

src/matchers/to-be-checked.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3-
import { computeAriaChecked, getRole, isAccessibilityElement } from '../helpers/accessibility';
3+
import {
4+
computeAriaChecked,
5+
getRole,
6+
isAccessibilityElement,
7+
rolesSupportingCheckedState,
8+
} from '../helpers/accessibility';
49
import { ErrorWithStack } from '../helpers/errors';
10+
import { isHostSwitch } from '../helpers/host-component-names';
511
import { checkHostElement, formatElement } from './utils';
612

713
export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstance) {
814
checkHostElement(element, toBeChecked, this);
915

10-
if (!hasValidAccessibilityRole(element)) {
16+
if (!isHostSwitch(element) && !isSupportedAccessibilityElement(element)) {
1117
throw new ErrorWithStack(
12-
`toBeChecked() works only on accessibility elements with "checkbox" or "radio" role.`,
18+
`toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role.`,
1319
toBeChecked,
1420
);
1521
}
@@ -28,11 +34,11 @@ export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstanc
2834
};
2935
}
3036

31-
function hasValidAccessibilityRole(element: ReactTestInstance) {
37+
function isSupportedAccessibilityElement(element: ReactTestInstance) {
3238
if (!isAccessibilityElement(element)) {
3339
return false;
3440
}
3541

3642
const role = getRole(element);
37-
return role === 'checkbox' || role === 'radio';
43+
return rolesSupportingCheckedState[role];
3844
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,14 +477,14 @@ describe('aria-checked prop', () => {
477477
});
478478

479479
test('supports aria-checked="mixed" prop', () => {
480-
render(<View accessible accessibilityRole="checkbox" aria-checked="mixed" />);
480+
render(<View accessible role="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

486486
test('supports default aria-checked prop', () => {
487-
render(<View accessible accessibilityRole="checkbox" />);
487+
render(<View accessible role="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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
TouchableOpacity,
88
TouchableWithoutFeedback,
99
View,
10+
Switch,
1011
} from 'react-native';
1112
import { render, screen } from '../..';
1213

@@ -426,7 +427,7 @@ describe('supports accessibility states', () => {
426427
expect(screen.queryByRole('checkbox', { checked: 'mixed' })).toBe(null);
427428
});
428429

429-
it('returns `mixed` checkboxes', () => {
430+
test('returns `mixed` checkboxes', () => {
430431
render(
431432
<TouchableOpacity accessibilityRole="checkbox" accessibilityState={{ checked: 'mixed' }} />,
432433
);
@@ -508,6 +509,14 @@ describe('supports accessibility states', () => {
508509
expect(screen.queryByRole('checkbox', { checked: false })).toBe(null);
509510
});
510511

512+
test('supports "Switch" component', () => {
513+
render(<Switch value={true} />);
514+
515+
expect(screen.getByRole('switch', { checked: true })).toBeTruthy();
516+
expect(screen.queryByRole('switch', { checked: false })).toBe(null);
517+
expect(screen.queryByRole('switch', { checked: 'mixed' })).toBe(null);
518+
});
519+
511520
test('supports aria-checked={true} prop', () => {
512521
render(<View accessible role="checkbox" aria-checked={true} />);
513522
expect(screen.getByRole('checkbox', { checked: true })).toBeTruthy();

website/docs/12.x/docs/api/jest-matchers.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ These allow you to assert whether the given element is checked or partially chec
140140

141141
:::note
142142

143-
- `toBeChecked()` matcher works only on elements with the `checkbox` or `radio` role.
144-
- `toBePartiallyChecked()` matcher works only on elements with the `checkbox` role.
143+
- `toBeChecked()` matcher works only on `Switch` host elements and accessibility elements with `checkbox`, `radio` or `switch` role.
144+
- `toBePartiallyChecked()` matcher works only on elements with `checkbox` role.
145145

146146
:::
147147

website/docs/12.x/docs/migration/jest-matchers.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,5 @@ New [`toHaveAccessibleName()`](docs/api/jest-matchers#tohaveaccessiblename) has
7171
You should be aware of the following details:
7272

7373
- [`toBeEnabled()` / `toBeDisabled()`](docs/api/jest-matchers#tobeenabled) matchers also check the disabled state for the element's ancestors and not only the element itself. This is the same as in legacy Jest Native matchers of the same name but differs from the removed `toHaveAccessibilityState()` matcher.
74-
- [`toBeChecked()`](docs/api/jest-matchers#tobechecked) matcher supports only elements with a `checkbox` or `radio` role
74+
- [`toBeChecked()`](docs/api/jest-matchers#tobechecked) matcher supports only elements with a `checkbox`, `radio` and 'switch' role
7575
- [`toBePartiallyChecked()`](docs/api/jest-matchers#tobechecked) matcher supports only elements with `checkbox` role

0 commit comments

Comments
 (0)