Skip to content

Commit

Permalink
feat: toBeChecked & toBePartiallyChecked matcher (#1479)
Browse files Browse the repository at this point in the history
* 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 <mdjastrzebski@gmail.com>
  • Loading branch information
kyawthura-gg and mdjastrzebski committed Sep 4, 2023
1 parent 2d96b77 commit 5a7c693
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 2 deletions.
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);
}

0 comments on commit 5a7c693

Please sign in to comment.