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') + }, + } +}