Skip to content

Commit 5a7c693

Browse files
feat: toBeChecked & toBePartiallyChecked matcher (#1479)
* feat: added toBeChecked & toBePartiallyChecked * test: wip test * fix: fix typo * test: added more test * fix: fixed throw error * test: added throw error test * refactor: refactor matchers * refactor: tweaks * refactor: final tweaks * chore: fix ts --------- Co-authored-by: Maciej Jastrzebski <[email protected]>
1 parent 2d96b77 commit 5a7c693

File tree

9 files changed

+396
-2
lines changed

9 files changed

+396
-2
lines changed

src/helpers/accessiblity.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,10 @@ export function getAccessibilityState(element: ReactTestInstance) {
160160
selected: ariaSelected ?? accessibilityState?.selected,
161161
};
162162
}
163+
164+
export function getAccessibilityCheckedState(
165+
element: ReactTestInstance
166+
): AccessibilityState['checked'] {
167+
const { accessibilityState, 'aria-checked': ariaChecked } = element.props;
168+
return ariaChecked ?? accessibilityState?.checked;
169+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import React from 'react';
2+
import { type AccessibilityRole, View } from 'react-native';
3+
import render from '../../render';
4+
import { screen } from '../../screen';
5+
import '../extend-expect';
6+
7+
function renderViewsWithRole(role: AccessibilityRole) {
8+
return render(
9+
<>
10+
<View
11+
testID={`${role}-checked`}
12+
accessible
13+
accessibilityRole={role}
14+
accessibilityState={{ checked: true }}
15+
/>
16+
<View
17+
testID={`${role}-unchecked`}
18+
accessible
19+
accessibilityRole={role}
20+
accessibilityState={{ checked: false }}
21+
/>
22+
<View
23+
testID={`${role}-mixed`}
24+
accessible
25+
accessibilityRole={role}
26+
accessibilityState={{ checked: 'mixed' }}
27+
/>
28+
<View testID={`${role}-default`} accessible accessibilityRole={role} />
29+
</>
30+
);
31+
}
32+
33+
test('toBeCheck() with checkbox role', () => {
34+
renderViewsWithRole('checkbox');
35+
36+
const checked = screen.getByTestId('checkbox-checked');
37+
const unchecked = screen.getByTestId('checkbox-unchecked');
38+
const mixed = screen.getByTestId('checkbox-mixed');
39+
const defaultView = screen.getByTestId('checkbox-default');
40+
41+
expect(checked).toBeChecked();
42+
expect(unchecked).not.toBeChecked();
43+
expect(mixed).not.toBeChecked();
44+
expect(defaultView).not.toBeChecked();
45+
46+
expect(() => expect(checked).not.toBeChecked())
47+
.toThrowErrorMatchingInlineSnapshot(`
48+
"expect(element).not.toBeChecked()
49+
50+
Received element is checked:
51+
<View
52+
accessibilityRole="checkbox"
53+
accessibilityState={
54+
{
55+
"checked": true,
56+
}
57+
}
58+
accessible={true}
59+
testID="checkbox-checked"
60+
/>"
61+
`);
62+
expect(() => expect(unchecked).toBeChecked())
63+
.toThrowErrorMatchingInlineSnapshot(`
64+
"expect(element).toBeChecked()
65+
66+
Received element is not checked:
67+
<View
68+
accessibilityRole="checkbox"
69+
accessibilityState={
70+
{
71+
"checked": false,
72+
}
73+
}
74+
accessible={true}
75+
testID="checkbox-unchecked"
76+
/>"
77+
`);
78+
expect(() => expect(mixed).toBeChecked()).toThrowErrorMatchingInlineSnapshot(`
79+
"expect(element).toBeChecked()
80+
81+
Received element is not checked:
82+
<View
83+
accessibilityRole="checkbox"
84+
accessibilityState={
85+
{
86+
"checked": "mixed",
87+
}
88+
}
89+
accessible={true}
90+
testID="checkbox-mixed"
91+
/>"
92+
`);
93+
expect(() => expect(defaultView).toBeChecked())
94+
.toThrowErrorMatchingInlineSnapshot(`
95+
"expect(element).toBeChecked()
96+
97+
Received element is not checked:
98+
<View
99+
accessibilityRole="checkbox"
100+
accessible={true}
101+
testID="checkbox-default"
102+
/>"
103+
`);
104+
});
105+
106+
test('toBeCheck() with radio role', () => {
107+
renderViewsWithRole('radio');
108+
109+
const checked = screen.getByTestId('radio-checked');
110+
const unchecked = screen.getByTestId('radio-unchecked');
111+
const defaultView = screen.getByTestId('radio-default');
112+
113+
expect(checked).toBeChecked();
114+
expect(unchecked).not.toBeChecked();
115+
expect(defaultView).not.toBeChecked();
116+
117+
expect(() => expect(checked).not.toBeChecked())
118+
.toThrowErrorMatchingInlineSnapshot(`
119+
"expect(element).not.toBeChecked()
120+
121+
Received element is checked:
122+
<View
123+
accessibilityRole="radio"
124+
accessibilityState={
125+
{
126+
"checked": true,
127+
}
128+
}
129+
accessible={true}
130+
testID="radio-checked"
131+
/>"
132+
`);
133+
expect(() => expect(unchecked).toBeChecked())
134+
.toThrowErrorMatchingInlineSnapshot(`
135+
"expect(element).toBeChecked()
136+
137+
Received element is not checked:
138+
<View
139+
accessibilityRole="radio"
140+
accessibilityState={
141+
{
142+
"checked": false,
143+
}
144+
}
145+
accessible={true}
146+
testID="radio-unchecked"
147+
/>"
148+
`);
149+
expect(() => expect(defaultView).toBeChecked())
150+
.toThrowErrorMatchingInlineSnapshot(`
151+
"expect(element).toBeChecked()
152+
153+
Received element is not checked:
154+
<View
155+
accessibilityRole="radio"
156+
accessible={true}
157+
testID="radio-default"
158+
/>"
159+
`);
160+
});
161+
162+
test('throws error for invalid role', () => {
163+
renderViewsWithRole('adjustable');
164+
165+
const checked = screen.getByTestId('adjustable-checked');
166+
const unchecked = screen.getByTestId('adjustable-unchecked');
167+
168+
expect(() =>
169+
expect(checked).toBeChecked()
170+
).toThrowErrorMatchingInlineSnapshot(
171+
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`
172+
);
173+
expect(() =>
174+
expect(unchecked).not.toBeChecked()
175+
).toThrowErrorMatchingInlineSnapshot(
176+
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`
177+
);
178+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React from 'react';
2+
import { type AccessibilityRole, View } from 'react-native';
3+
import render from '../../render';
4+
import { screen } from '../../screen';
5+
import '../extend-expect';
6+
7+
function renderViewsWithRole(role: AccessibilityRole) {
8+
return render(
9+
<>
10+
<View
11+
testID={`${role}-checked`}
12+
accessible
13+
accessibilityRole={role}
14+
accessibilityState={{ checked: true }}
15+
/>
16+
<View
17+
testID={`${role}-unchecked`}
18+
accessible
19+
accessibilityRole={role}
20+
accessibilityState={{ checked: false }}
21+
/>
22+
<View
23+
testID={`${role}-mixed`}
24+
accessible
25+
accessibilityRole={role}
26+
accessibilityState={{ checked: 'mixed' }}
27+
/>
28+
<View testID={`${role}-default`} accessible accessibilityRole={role} />
29+
</>
30+
);
31+
}
32+
33+
test('toBePartiallyCheck() with checkbox role', () => {
34+
renderViewsWithRole('checkbox');
35+
36+
const checked = screen.getByTestId('checkbox-checked');
37+
const unchecked = screen.getByTestId('checkbox-unchecked');
38+
const mixed = screen.getByTestId('checkbox-mixed');
39+
const defaultView = screen.getByTestId('checkbox-default');
40+
41+
expect(mixed).toBePartiallyChecked();
42+
43+
expect(checked).not.toBePartiallyChecked();
44+
expect(unchecked).not.toBePartiallyChecked();
45+
expect(defaultView).not.toBePartiallyChecked();
46+
47+
expect(() => expect(mixed).not.toBePartiallyChecked())
48+
.toThrowErrorMatchingInlineSnapshot(`
49+
"expect(element).not.toBePartiallyChecked()
50+
51+
Received element is partially checked:
52+
<View
53+
accessibilityRole="checkbox"
54+
accessibilityState={
55+
{
56+
"checked": "mixed",
57+
}
58+
}
59+
accessible={true}
60+
testID="checkbox-mixed"
61+
/>"
62+
`);
63+
64+
expect(() => expect(checked).toBePartiallyChecked())
65+
.toThrowErrorMatchingInlineSnapshot(`
66+
"expect(element).toBePartiallyChecked()
67+
68+
Received element is not partially checked:
69+
<View
70+
accessibilityRole="checkbox"
71+
accessibilityState={
72+
{
73+
"checked": true,
74+
}
75+
}
76+
accessible={true}
77+
testID="checkbox-checked"
78+
/>"
79+
`);
80+
expect(() => expect(defaultView).toBePartiallyChecked())
81+
.toThrowErrorMatchingInlineSnapshot(`
82+
"expect(element).toBePartiallyChecked()
83+
84+
Received element is not partially checked:
85+
<View
86+
accessibilityRole="checkbox"
87+
accessible={true}
88+
testID="checkbox-default"
89+
/>"
90+
`);
91+
});
92+
93+
test('toBeCheck() with radio role', () => {
94+
renderViewsWithRole('radio');
95+
96+
const checked = screen.getByTestId('radio-checked');
97+
const mixed = screen.getByTestId('radio-mixed');
98+
99+
expect(() =>
100+
expect(checked).toBePartiallyChecked()
101+
).toThrowErrorMatchingInlineSnapshot(
102+
`"toBePartiallyChecked() works only on accessibility elements with "checkbox" role."`
103+
);
104+
expect(() =>
105+
expect(mixed).toBePartiallyChecked()
106+
).toThrowErrorMatchingInlineSnapshot(
107+
`"toBePartiallyChecked() works only on accessibility elements with "checkbox" role."`
108+
);
109+
});

src/matchers/extend-expect.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import type { TextMatch, TextMatchOptions } from '../matches';
22

33
export interface JestNativeMatchers<R> {
44
toBeOnTheScreen(): R;
5+
toBeChecked(): R;
56
toBeDisabled(): R;
67
toBeEmptyElement(): R;
78
toBeEnabled(): R;
9+
toBePartiallyChecked(): R;
810
toBeVisible(): R;
911
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
1012
toHaveProp(name: string, expectedValue?: unknown): R;

src/matchers/extend-expect.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
/// <reference path="./extend-expect.d.ts" />
22

33
import { toBeOnTheScreen } from './to-be-on-the-screen';
4+
import { toBeChecked } from './to-be-checked';
45
import { toBeDisabled, toBeEnabled } from './to-be-disabled';
56
import { toBeEmptyElement } from './to-be-empty-element';
7+
import { toBePartiallyChecked } from './to-be-partially-checked';
68
import { toBeVisible } from './to-be-visible';
79
import { toHaveDisplayValue } from './to-have-display-value';
810
import { toHaveProp } from './to-have-prop';
911
import { toHaveTextContent } from './to-have-text-content';
1012

1113
expect.extend({
1214
toBeOnTheScreen,
15+
toBeChecked,
1316
toBeDisabled,
1417
toBeEmptyElement,
1518
toBeEnabled,
19+
toBePartiallyChecked,
1620
toBeVisible,
1721
toHaveDisplayValue,
1822
toHaveProp,

src/matchers/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
export { toBeOnTheScreen } from './to-be-on-the-screen';
2+
export { toBeChecked } from './to-be-checked';
3+
export { toBeDisabled, toBeEnabled } from './to-be-disabled';
24
export { toBeEmptyElement } from './to-be-empty-element';
5+
export { toBePartiallyChecked } from './to-be-partially-checked';
36
export { toBeVisible } from './to-be-visible';
7+
export { toHaveDisplayValue } from './to-have-display-value';
8+
export { toHaveProp } from './to-have-prop';
9+
export { toHaveTextContent } from './to-have-text-content';

src/matchers/to-be-checked.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { ReactTestInstance } from 'react-test-renderer';
2+
import { matcherHint } from 'jest-matcher-utils';
3+
import {
4+
getAccessibilityCheckedState,
5+
getAccessibilityRole,
6+
isAccessibilityElement,
7+
} from '../helpers/accessiblity';
8+
import { ErrorWithStack } from '../helpers/errors';
9+
import { checkHostElement, formatElement } from './utils';
10+
11+
export function toBeChecked(
12+
this: jest.MatcherContext,
13+
element: ReactTestInstance
14+
) {
15+
checkHostElement(element, toBeChecked, this);
16+
17+
if (!hasValidAccessibilityRole(element)) {
18+
throw new ErrorWithStack(
19+
`toBeChecked() works only on accessibility elements with "checkbox" or "radio" role.`,
20+
toBeChecked
21+
);
22+
}
23+
24+
return {
25+
pass: getAccessibilityCheckedState(element) === true,
26+
message: () => {
27+
const is = this.isNot ? 'is' : 'is not';
28+
return [
29+
matcherHint(`${this.isNot ? '.not' : ''}.toBeChecked`, 'element', ''),
30+
'',
31+
`Received element ${is} checked:`,
32+
formatElement(element),
33+
].join('\n');
34+
},
35+
};
36+
}
37+
38+
const VALID_ROLES = new Set(['checkbox', 'radio']);
39+
40+
function hasValidAccessibilityRole(element: ReactTestInstance) {
41+
const role = getAccessibilityRole(element);
42+
return isAccessibilityElement(element) && VALID_ROLES.has(role);
43+
}

0 commit comments

Comments
 (0)