From 75d12c4d2d224e481ddc8f434aadddeba3e74e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Apr 2018 23:35:28 -0300 Subject: [PATCH] feat(matchers): add toHaveClass custom matcher (closes #2) (#4) * Add toHaveClass custom matcher * Add documentation in the README * Handle the case where an element has no class --- README.md | 20 +++++++++++++++ src/__tests__/element-queries.js | 43 ++++++++++++++++++++++++++++++++ src/extend-expect.js | 9 +++++-- src/jest-extensions.js | 34 +++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05763937..50cf689a 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ when a real user uses it. * [`toBeInTheDOM`](#tobeinthedom) * [`toHaveTextContent`](#tohavetextcontent) * [`toHaveAttribute`](#tohaveattribute) + * [`toHaveClass`](#tohaveclass) * [Custom Jest Matchers - Typescript](#custom-jest-matchers---typescript) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) @@ -364,6 +365,25 @@ expect(getByTestId(container, 'ok-button')).not.toHaveAttribute( // ... ``` +### `toHaveClass` + +This allows you to check wether the given element has certain classes within its +`class` attribute. + +```javascript +// add the custom expect matchers +import 'dom-testing-library/extend-expect' + +// ... +// +expect(getByTestId(container, 'delete-button')).toHaveClass('extra') +expect(getByTestId(container, 'delete-button')).toHaveClass('btn-danger btn') +expect(getByTestId(container, 'delete-button')).not.toHaveClass('btn-link') +// ... +``` + ### Custom Jest Matchers - Typescript When you use custom Jest Matchers with Typescript, you will need to extend the diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index 39a0eecc..33ff6100 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -150,4 +150,47 @@ test('using jest helpers to check element attributes', () => { ).toThrowError() }) +test('using jest helpers to check element class names', () => { + const {getByTestId} = render(` +
+ + +
+ `) + + expect(getByTestId('delete-button')).toHaveClass('btn') + expect(getByTestId('delete-button')).toHaveClass('btn-danger') + expect(getByTestId('delete-button')).toHaveClass('extra') + expect(getByTestId('delete-button')).not.toHaveClass('xtra') + expect(getByTestId('delete-button')).toHaveClass('btn btn-danger') + expect(getByTestId('delete-button')).not.toHaveClass('btn-link') + expect(getByTestId('cancel-button')).not.toHaveClass('btn-danger') + + expect(() => + expect(getByTestId('delete-button')).not.toHaveClass('btn'), + ).toThrowError() + expect(() => + expect(getByTestId('delete-button')).not.toHaveClass('btn-danger'), + ).toThrowError() + expect(() => + expect(getByTestId('delete-button')).not.toHaveClass('extra'), + ).toThrowError() + expect(() => + expect(getByTestId('delete-button')).toHaveClass('xtra'), + ).toThrowError() + expect(() => + expect(getByTestId('delete-button')).not.toHaveClass('btn btn-danger'), + ).toThrowError() + expect(() => + expect(getByTestId('delete-button')).toHaveClass('btn-link'), + ).toThrowError() + expect(() => + expect(getByTestId('cancel-button')).toHaveClass('btn-danger'), + ).toThrowError() +}) + /* eslint jsx-a11y/label-has-for:0 */ diff --git a/src/extend-expect.js b/src/extend-expect.js index a2ddc2b4..a18d54f3 100644 --- a/src/extend-expect.js +++ b/src/extend-expect.js @@ -1,4 +1,9 @@ import extensions from './jest-extensions' -const {toBeInTheDOM, toHaveTextContent, toHaveAttribute} = extensions -expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute}) +const { + toBeInTheDOM, + toHaveTextContent, + toHaveAttribute, + toHaveClass, +} = extensions +expect.extend({toBeInTheDOM, toHaveTextContent, toHaveAttribute, toHaveClass}) diff --git a/src/jest-extensions.js b/src/jest-extensions.js index 228a40e2..a44e728d 100644 --- a/src/jest-extensions.js +++ b/src/jest-extensions.js @@ -49,6 +49,17 @@ function getAttributeComment(name, value) { : `element.getAttribute(${stringify(name)}) === ${stringify(value)}` } +function splitClassNames(str) { + if (!str) { + return [] + } + return str.split(/\s+/).filter(s => s.length > 0) +} + +function isSubset(subset, superset) { + return subset.every(item => superset.includes(item)) +} + const extensions = { toBeInTheDOM(received) { if (received) { @@ -130,6 +141,29 @@ const extensions = { }, } }, + + toHaveClass(htmlElement, expectedClassNames) { + checkHtmlElement(htmlElement) + const received = splitClassNames(htmlElement.getAttribute('class')) + const expected = splitClassNames(expectedClassNames) + return { + pass: isSubset(expected, received), + message: () => { + const to = this.isNot ? 'not to' : 'to' + return getMessage( + matcherHint( + `${this.isNot ? '.not' : ''}.toHaveClass`, + 'element', + printExpected(expected.join(' ')), + ), + `Expected the element ${to} have class`, + expected.join(' '), + 'Received', + received.join(' '), + ) + }, + } + }, } export default extensions