diff --git a/README.md b/README.md
index b21a10d9..c3b57bc5 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,6 @@ clear to read and to maintain.
-
- [Installation](#installation)
- [Usage](#usage)
- [Custom matchers](#custom-matchers)
@@ -69,6 +68,7 @@ clear to read and to maintain.
- [`toHaveValue`](#tohavevalue)
- [`toHaveDisplayValue`](#tohavedisplayvalue)
- [`toBeChecked`](#tobechecked)
+ - [`toBePartiallyChecked`](#tobepartiallychecked)
- [`toHaveDescription`](#tohavedescription)
- [Deprecated matchers](#deprecated-matchers)
- [`toBeInTheDOM`](#tobeinthedom)
@@ -895,6 +895,52 @@ expect(ariaSwitchUnchecked).not.toBeChecked()
+### `toBePartiallyChecked`
+
+```typescript
+toBePartiallyChecked()
+```
+
+This allows you to check whether the given element is partially checked. It
+accepts an `input` of type `checkbox` and elements with a `role` of `checkbox`
+with a `aria-checked="mixed"`, or `input` of type `checkbox` with
+`indeterminate` set to `true`
+
+#### Examples
+
+```html
+
+
+
+
+
+
+```
+
+```javascript
+const ariaCheckboxMixed = getByTestId('aria-checkbox-mixed')
+const inputCheckboxChecked = getByTestId('input-checkbox-checked')
+const inputCheckboxUnchecked = getByTestId('input-checkbox-unchecked')
+const ariaCheckboxChecked = getByTestId('aria-checkbox-checked')
+const ariaCheckboxUnchecked = getByTestId('aria-checkbox-unchecked')
+const inputCheckboxIndeterminate = getByTestId('input-checkbox-indeterminate')
+
+expect(ariaCheckboxMixed).toBePartiallyChecked()
+expect(inputCheckboxChecked).not.toBePartiallyChecked()
+expect(inputCheckboxUnchecked).not.toBePartiallyChecked()
+expect(ariaCheckboxChecked).not.toBePartiallyChecked()
+expect(ariaCheckboxUnchecked).not.toBePartiallyChecked()
+
+inputCheckboxIndeterminate.indeterminate = true
+expect(inputCheckboxIndeterminate).toBePartiallyChecked()
+```
+
+
+
### `toHaveDescription`
```typescript
diff --git a/src/__tests__/to-be-partially-checked.js b/src/__tests__/to-be-partially-checked.js
new file mode 100644
index 00000000..8b0f4c7f
--- /dev/null
+++ b/src/__tests__/to-be-partially-checked.js
@@ -0,0 +1,122 @@
+import {render} from './helpers/test-utils'
+
+describe('.toBePartiallyChecked', () => {
+ test('handles input checkbox with aria-checked', () => {
+ const {queryByTestId} = render(`
+
+
+
+ `)
+
+ expect(queryByTestId('checkbox-mixed')).toBePartiallyChecked()
+ expect(queryByTestId('checkbox-checked')).not.toBePartiallyChecked()
+ expect(queryByTestId('checkbox-unchecked')).not.toBePartiallyChecked()
+ })
+
+ test('handles input checkbox set as indeterminate', () => {
+ const {queryByTestId} = render(`
+
+
+
+ `)
+
+ queryByTestId('checkbox-mixed').indeterminate = true
+
+ expect(queryByTestId('checkbox-mixed')).toBePartiallyChecked()
+ expect(queryByTestId('checkbox-checked')).not.toBePartiallyChecked()
+ expect(queryByTestId('checkbox-unchecked')).not.toBePartiallyChecked()
+ })
+
+ test('handles element with role="checkbox"', () => {
+ const {queryByTestId} = render(`
+
+
+
+ `)
+
+ expect(queryByTestId('aria-checkbox-mixed')).toBePartiallyChecked()
+ expect(queryByTestId('aria-checkbox-checked')).not.toBePartiallyChecked()
+ expect(queryByTestId('aria-checkbox-unchecked')).not.toBePartiallyChecked()
+ })
+
+ test('throws when input checkbox is mixed but expected not to be', () => {
+ const {queryByTestId} = render(
+ ``,
+ )
+
+ expect(() =>
+ expect(queryByTestId('checkbox-mixed')).not.toBePartiallyChecked(),
+ ).toThrowError()
+ })
+
+ test('throws when input checkbox is indeterminate but expected not to be', () => {
+ const {queryByTestId} = render(
+ ``,
+ )
+
+ queryByTestId('checkbox-mixed').indeterminate = true
+
+ expect(() =>
+ expect(queryByTestId('input-mixed')).not.toBePartiallyChecked(),
+ ).toThrowError()
+ })
+
+ test('throws when input checkbox is not checked but expected to be', () => {
+ const {queryByTestId} = render(
+ ``,
+ )
+
+ expect(() =>
+ expect(queryByTestId('checkbox-empty')).toBePartiallyChecked(),
+ ).toThrowError()
+ })
+
+ test('throws when element with role="checkbox" is partially checked but expected not to be', () => {
+ const {queryByTestId} = render(
+ ``,
+ )
+
+ expect(() =>
+ expect(queryByTestId('aria-checkbox-mixed')).not.toBePartiallyChecked(),
+ ).toThrowError()
+ })
+
+ test('throws when element with role="checkbox" is checked but expected to be partially checked', () => {
+ const {queryByTestId} = render(
+ ``,
+ )
+
+ expect(() =>
+ expect(queryByTestId('aria-checkbox-checked')).toBePartiallyChecked(),
+ ).toThrowError()
+ })
+
+ test('throws when element with role="checkbox" is not checked but expected to be', () => {
+ const {queryByTestId} = render(
+ ``,
+ )
+
+ expect(() =>
+ expect(queryByTestId('aria-checkbox')).toBePartiallyChecked(),
+ ).toThrowError()
+ })
+
+ test('throws when element with role="checkbox" has an invalid aria-checked attribute', () => {
+ const {queryByTestId} = render(
+ ``,
+ )
+
+ expect(() =>
+ expect(queryByTestId('aria-checkbox-invalid')).toBePartiallyChecked(),
+ ).toThrowError()
+ })
+
+ test('throws when the element is not a checkbox', () => {
+ const {queryByTestId} = render(``)
+ expect(() =>
+ expect(queryByTestId('select')).toBePartiallyChecked(),
+ ).toThrowError(
+ 'only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked(). Use .toHaveValue() instead',
+ )
+ })
+})
diff --git a/src/matchers.js b/src/matchers.js
index 703dd18c..b9b500d3 100644
--- a/src/matchers.js
+++ b/src/matchers.js
@@ -16,6 +16,7 @@ import {toBeInvalid, toBeValid} from './to-be-invalid'
import {toHaveValue} from './to-have-value'
import {toHaveDisplayValue} from './to-have-display-value'
import {toBeChecked} from './to-be-checked'
+import {toBePartiallyChecked} from './to-be-partially-checked'
import {toHaveDescription} from './to-have-description'
export {
@@ -39,5 +40,6 @@ export {
toHaveValue,
toHaveDisplayValue,
toBeChecked,
+ toBePartiallyChecked,
toHaveDescription,
}
diff --git a/src/to-be-partially-checked.js b/src/to-be-partially-checked.js
new file mode 100644
index 00000000..1b284985
--- /dev/null
+++ b/src/to-be-partially-checked.js
@@ -0,0 +1,51 @@
+import {matcherHint, printReceived} from 'jest-matcher-utils'
+import {checkHtmlElement} from './utils'
+
+export function toBePartiallyChecked(element) {
+ checkHtmlElement(element, toBePartiallyChecked, this)
+
+ const isValidInput = () => {
+ return (
+ element.tagName.toLowerCase() === 'input' && element.type === 'checkbox'
+ )
+ }
+
+ const isValidAriaElement = () => {
+ return element.getAttribute('role') === 'checkbox'
+ }
+
+ if (!isValidInput() && !isValidAriaElement()) {
+ return {
+ pass: false,
+ message: () =>
+ 'only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked(). Use .toHaveValue() instead',
+ }
+ }
+
+ const isPartiallyChecked = () => {
+ const isAriaMixed = element.getAttribute('aria-checked') === 'mixed'
+
+ if (isValidInput()) {
+ return element.indeterminate || isAriaMixed
+ }
+
+ return isAriaMixed
+ }
+
+ return {
+ pass: isPartiallyChecked(),
+ message: () => {
+ const is = isPartiallyChecked() ? 'is' : 'is not'
+ return [
+ matcherHint(
+ `${this.isNot ? '.not' : ''}.toBePartiallyChecked`,
+ 'element',
+ '',
+ ),
+ '',
+ `Received element ${is} partially checked:`,
+ ` ${printReceived(element.cloneNode(false))}`,
+ ].join('\n')
+ },
+ }
+}