Skip to content
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

feat: toBeChecked & toBePartiallyChecked matcher #1479

Merged
merged 10 commits into from
Sep 4, 2023
Merged
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);
}