Skip to content

feat: toBeChecked & toBePartiallyChecked matcher #1479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/helpers/accessiblity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,10 @@ export function getAccessibilityState(element: ReactTestInstance) {
selected: ariaSelected ?? accessibilityState?.selected,
};
}

export function getAccessibilityCheckedState(
element: ReactTestInstance
): AccessibilityState['checked'] {
const { accessibilityState, 'aria-checked': ariaChecked } = element.props;
return ariaChecked ?? accessibilityState?.checked;
}
178 changes: 178 additions & 0 deletions src/matchers/__tests__/to-be-checked.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React from 'react';
import { type AccessibilityRole, View } from 'react-native';
import render from '../../render';
import { screen } from '../../screen';
import '../extend-expect';

function renderViewsWithRole(role: AccessibilityRole) {
return render(
<>
<View
testID={`${role}-checked`}
accessible
accessibilityRole={role}
accessibilityState={{ checked: true }}
/>
<View
testID={`${role}-unchecked`}
accessible
accessibilityRole={role}
accessibilityState={{ checked: false }}
/>
<View
testID={`${role}-mixed`}
accessible
accessibilityRole={role}
accessibilityState={{ checked: 'mixed' }}
/>
<View testID={`${role}-default`} accessible accessibilityRole={role} />
</>
);
}

test('toBeCheck() with checkbox role', () => {
renderViewsWithRole('checkbox');

const checked = screen.getByTestId('checkbox-checked');
const unchecked = screen.getByTestId('checkbox-unchecked');
const mixed = screen.getByTestId('checkbox-mixed');
const defaultView = screen.getByTestId('checkbox-default');

expect(checked).toBeChecked();
expect(unchecked).not.toBeChecked();
expect(mixed).not.toBeChecked();
expect(defaultView).not.toBeChecked();

expect(() => expect(checked).not.toBeChecked())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBeChecked()

Received element is checked:
<View
accessibilityRole="checkbox"
accessibilityState={
{
"checked": true,
}
}
accessible={true}
testID="checkbox-checked"
/>"
`);
expect(() => expect(unchecked).toBeChecked())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeChecked()

Received element is not checked:
<View
accessibilityRole="checkbox"
accessibilityState={
{
"checked": false,
}
}
accessible={true}
testID="checkbox-unchecked"
/>"
`);
expect(() => expect(mixed).toBeChecked()).toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeChecked()

Received element is not checked:
<View
accessibilityRole="checkbox"
accessibilityState={
{
"checked": "mixed",
}
}
accessible={true}
testID="checkbox-mixed"
/>"
`);
expect(() => expect(defaultView).toBeChecked())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeChecked()

Received element is not checked:
<View
accessibilityRole="checkbox"
accessible={true}
testID="checkbox-default"
/>"
`);
});

test('toBeCheck() with radio role', () => {
renderViewsWithRole('radio');

const checked = screen.getByTestId('radio-checked');
const unchecked = screen.getByTestId('radio-unchecked');
const defaultView = screen.getByTestId('radio-default');

expect(checked).toBeChecked();
expect(unchecked).not.toBeChecked();
expect(defaultView).not.toBeChecked();

expect(() => expect(checked).not.toBeChecked())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBeChecked()

Received element is checked:
<View
accessibilityRole="radio"
accessibilityState={
{
"checked": true,
}
}
accessible={true}
testID="radio-checked"
/>"
`);
expect(() => expect(unchecked).toBeChecked())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeChecked()

Received element is not checked:
<View
accessibilityRole="radio"
accessibilityState={
{
"checked": false,
}
}
accessible={true}
testID="radio-unchecked"
/>"
`);
expect(() => expect(defaultView).toBeChecked())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeChecked()

Received element is not checked:
<View
accessibilityRole="radio"
accessible={true}
testID="radio-default"
/>"
`);
});

test('throws error for invalid role', () => {
renderViewsWithRole('adjustable');

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

expect(() =>
expect(checked).toBeChecked()
).toThrowErrorMatchingInlineSnapshot(
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`
);
expect(() =>
expect(unchecked).not.toBeChecked()
).toThrowErrorMatchingInlineSnapshot(
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`
);
});
109 changes: 109 additions & 0 deletions src/matchers/__tests__/to-be-partially-checked.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import { type AccessibilityRole, View } from 'react-native';
import render from '../../render';
import { screen } from '../../screen';
import '../extend-expect';

function renderViewsWithRole(role: AccessibilityRole) {
return render(
<>
<View
testID={`${role}-checked`}
accessible
accessibilityRole={role}
accessibilityState={{ checked: true }}
/>
<View
testID={`${role}-unchecked`}
accessible
accessibilityRole={role}
accessibilityState={{ checked: false }}
/>
<View
testID={`${role}-mixed`}
accessible
accessibilityRole={role}
accessibilityState={{ checked: 'mixed' }}
/>
<View testID={`${role}-default`} accessible accessibilityRole={role} />
</>
);
}

test('toBePartiallyCheck() with checkbox role', () => {
renderViewsWithRole('checkbox');

const checked = screen.getByTestId('checkbox-checked');
const unchecked = screen.getByTestId('checkbox-unchecked');
const mixed = screen.getByTestId('checkbox-mixed');
const defaultView = screen.getByTestId('checkbox-default');

expect(mixed).toBePartiallyChecked();

expect(checked).not.toBePartiallyChecked();
expect(unchecked).not.toBePartiallyChecked();
expect(defaultView).not.toBePartiallyChecked();

expect(() => expect(mixed).not.toBePartiallyChecked())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBePartiallyChecked()

Received element is partially checked:
<View
accessibilityRole="checkbox"
accessibilityState={
{
"checked": "mixed",
}
}
accessible={true}
testID="checkbox-mixed"
/>"
`);

expect(() => expect(checked).toBePartiallyChecked())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBePartiallyChecked()

Received element is not partially checked:
<View
accessibilityRole="checkbox"
accessibilityState={
{
"checked": true,
}
}
accessible={true}
testID="checkbox-checked"
/>"
`);
expect(() => expect(defaultView).toBePartiallyChecked())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBePartiallyChecked()

Received element is not partially checked:
<View
accessibilityRole="checkbox"
accessible={true}
testID="checkbox-default"
/>"
`);
});

test('toBeCheck() with radio role', () => {
renderViewsWithRole('radio');

const checked = screen.getByTestId('radio-checked');
const mixed = screen.getByTestId('radio-mixed');

expect(() =>
expect(checked).toBePartiallyChecked()
).toThrowErrorMatchingInlineSnapshot(
`"toBePartiallyChecked() works only on accessibility elements with "checkbox" role."`
);
expect(() =>
expect(mixed).toBePartiallyChecked()
).toThrowErrorMatchingInlineSnapshot(
`"toBePartiallyChecked() works only on accessibility elements with "checkbox" role."`
);
});
2 changes: 2 additions & 0 deletions src/matchers/extend-expect.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import type { TextMatch, TextMatchOptions } from '../matches';

export interface JestNativeMatchers<R> {
toBeOnTheScreen(): R;
toBeChecked(): R;
toBeDisabled(): R;
toBeEmptyElement(): R;
toBeEnabled(): R;
toBePartiallyChecked(): R;
toBeVisible(): R;
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
toHaveProp(name: string, expectedValue?: unknown): R;
Expand Down
4 changes: 4 additions & 0 deletions src/matchers/extend-expect.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
/// <reference path="./extend-expect.d.ts" />

import { toBeOnTheScreen } from './to-be-on-the-screen';
import { toBeChecked } from './to-be-checked';
import { toBeDisabled, toBeEnabled } from './to-be-disabled';
import { toBeEmptyElement } from './to-be-empty-element';
import { toBePartiallyChecked } from './to-be-partially-checked';
import { toBeVisible } from './to-be-visible';
import { toHaveDisplayValue } from './to-have-display-value';
import { toHaveProp } from './to-have-prop';
import { toHaveTextContent } from './to-have-text-content';

expect.extend({
toBeOnTheScreen,
toBeChecked,
toBeDisabled,
toBeEmptyElement,
toBeEnabled,
toBePartiallyChecked,
toBeVisible,
toHaveDisplayValue,
toHaveProp,
Expand Down
6 changes: 6 additions & 0 deletions src/matchers/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export { toBeOnTheScreen } from './to-be-on-the-screen';
export { toBeChecked } from './to-be-checked';
export { toBeDisabled, toBeEnabled } from './to-be-disabled';
export { toBeEmptyElement } from './to-be-empty-element';
export { toBePartiallyChecked } from './to-be-partially-checked';
export { toBeVisible } from './to-be-visible';
export { toHaveDisplayValue } from './to-have-display-value';
export { toHaveProp } from './to-have-prop';
export { toHaveTextContent } from './to-have-text-content';
43 changes: 43 additions & 0 deletions src/matchers/to-be-checked.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
import {
getAccessibilityCheckedState,
getAccessibilityRole,
isAccessibilityElement,
} from '../helpers/accessiblity';
import { ErrorWithStack } from '../helpers/errors';
import { checkHostElement, formatElement } from './utils';

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

if (!hasValidAccessibilityRole(element)) {
throw new ErrorWithStack(
`toBeChecked() works only on accessibility elements with "checkbox" or "radio" role.`,
toBeChecked
);
}

return {
pass: getAccessibilityCheckedState(element) === true,
message: () => {
const is = this.isNot ? 'is' : 'is not';
return [
matcherHint(`${this.isNot ? '.not' : ''}.toBeChecked`, 'element', ''),
'',
`Received element ${is} checked:`,
formatElement(element),
].join('\n');
},
};
}

const VALID_ROLES = new Set(['checkbox', 'radio']);

function hasValidAccessibilityRole(element: ReactTestInstance) {
const role = getAccessibilityRole(element);
return isAccessibilityElement(element) && VALID_ROLES.has(role);
}
Loading