diff --git a/README.md b/README.md index a0b3b97a..9b34ebd6 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ clear to read and to maintain. + - [Installation](#installation) - [Usage](#usage) - [Custom matchers](#custom-matchers) @@ -498,7 +499,7 @@ expect(button).toHaveAttribute('type', expect.not.stringContaining('but')) ### `toHaveClass` ```typescript -toHaveClass(...classNames: string[]) +toHaveClass(...classNames: string[], options?: {exact: boolean}) ``` This allows you to check whether the given element has certain classes within @@ -525,6 +526,9 @@ expect(deleteButton).toHaveClass('btn-danger btn') expect(deleteButton).toHaveClass('btn-danger', 'btn') expect(deleteButton).not.toHaveClass('btn-link') +expect(deleteButton).toHaveClass('btn-danger extra btn', {exact: true}) // to check if the element has EXACTLY a set of classes +expect(deleteButton).not.toHaveClass('btn-danger extra', {exact: true}) // if it has more than expected it is going to fail + expect(noClasses).not.toHaveClass() ``` @@ -940,6 +944,7 @@ Thanks goes to these people ([emoji key][emojis]): + This project follows the [all-contributors][all-contributors] specification. diff --git a/src/__tests__/to-have-class.js b/src/__tests__/to-have-class.js index 4dd79c33..2e766518 100644 --- a/src/__tests__/to-have-class.js +++ b/src/__tests__/to-have-class.js @@ -2,21 +2,25 @@ import {render} from './helpers/test-utils' +const renderElementWithClasses = () => + render(` +
+ + + + + +
+
+
+`) + test('.toHaveClass', () => { - const {queryByTestId} = render(` -
- - - - - -
-
- `) + const {queryByTestId} = renderElementWithClasses() expect(queryByTestId('delete-button')).toHaveClass('btn') expect(queryByTestId('delete-button')).toHaveClass('btn-danger') @@ -91,3 +95,89 @@ test('.toHaveClass', () => { expect(queryByTestId('delete-button')).not.toHaveClass(' '), ).toThrowError(/(none)/) }) + +test('.toHaveClass with exact mode option', () => { + const {queryByTestId} = renderElementWithClasses() + + expect(queryByTestId('delete-button')).toHaveClass('btn extra btn-danger', { + exact: true, + }) + expect(queryByTestId('delete-button')).not.toHaveClass('btn extra', { + exact: true, + }) + expect( + queryByTestId('delete-button'), + ).not.toHaveClass('btn extra btn-danger foo', {exact: true}) + + expect(queryByTestId('delete-button')).toHaveClass('btn extra btn-danger', { + exact: false, + }) + expect(queryByTestId('delete-button')).toHaveClass('btn extra', { + exact: false, + }) + expect( + queryByTestId('delete-button'), + ).not.toHaveClass('btn extra btn-danger foo', {exact: false}) + + expect(queryByTestId('delete-button')).toHaveClass( + 'btn', + 'extra', + 'btn-danger', + {exact: true}, + ) + expect(queryByTestId('delete-button')).not.toHaveClass('btn', 'extra', { + exact: true, + }) + expect(queryByTestId('delete-button')).not.toHaveClass( + 'btn', + 'extra', + 'btn-danger', + 'foo', + {exact: true}, + ) + + expect(queryByTestId('delete-button')).toHaveClass( + 'btn', + 'extra', + 'btn-danger', + {exact: false}, + ) + expect(queryByTestId('delete-button')).toHaveClass('btn', 'extra', { + exact: false, + }) + expect(queryByTestId('delete-button')).not.toHaveClass( + 'btn', + 'extra', + 'btn-danger', + 'foo', + {exact: false}, + ) + + expect(queryByTestId('only-one-class')).toHaveClass('alone', {exact: true}) + expect(queryByTestId('only-one-class')).not.toHaveClass('alone foo', { + exact: true, + }) + expect(queryByTestId('only-one-class')).not.toHaveClass('alone', 'foo', { + exact: true, + }) + + expect(queryByTestId('only-one-class')).toHaveClass('alone', {exact: false}) + expect(queryByTestId('only-one-class')).not.toHaveClass('alone foo', { + exact: false, + }) + expect(queryByTestId('only-one-class')).not.toHaveClass('alone', 'foo', { + exact: false, + }) + + expect(() => + expect(queryByTestId('only-one-class')).not.toHaveClass('alone', { + exact: true, + }), + ).toThrowError(/Expected the element not to have EXACTLY defined classes/) + + expect(() => + expect(queryByTestId('only-one-class')).toHaveClass('alone', 'foo', { + exact: true, + }), + ).toThrowError(/Expected the element to have EXACTLY defined classes/) +}) diff --git a/src/to-have-class.js b/src/to-have-class.js index 9d228864..99b58892 100644 --- a/src/to-have-class.js +++ b/src/to-have-class.js @@ -1,6 +1,20 @@ import {matcherHint, printExpected} from 'jest-matcher-utils' import {checkHtmlElement, getMessage} from './utils' +function getExpectedClassNamesAndOptions(params) { + const lastParam = params.pop() + let expectedClassNames, options + + if (typeof lastParam === 'object') { + expectedClassNames = params + options = lastParam + } else { + expectedClassNames = params.concat(lastParam) + options = { exact: false } + } + return {expectedClassNames, options} +} + function splitClassNames(str) { if (!str) { return [] @@ -12,13 +26,31 @@ function isSubset(subset, superset) { return subset.every(item => superset.includes(item)) } -export function toHaveClass(htmlElement, ...expectedClassNames) { +export function toHaveClass(htmlElement, ...params) { checkHtmlElement(htmlElement, toHaveClass, this) + const {expectedClassNames, options} = getExpectedClassNamesAndOptions(params) + const received = splitClassNames(htmlElement.getAttribute('class')) const expected = expectedClassNames.reduce( (acc, className) => acc.concat(splitClassNames(className)), [], ) + + if (options.exact) { + return { + pass: isSubset(expected, received) && expected.length === received.length, + message: () => { + const to = this.isNot ? 'not to' : 'to' + return getMessage( + `Expected the element ${to} have EXACTLY defined classes`, + expected.join(' '), + 'Received', + received.join(' '), + ) + }, + } + } + return expected.length > 0 ? { pass: isSubset(expected, received),