diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 811c0130..7f5e459d 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -159,6 +159,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) { 'toHaveBeenCalledTimes', 'toHaveBeenCalledWith', 'toHaveClass', + 'toHaveClasses', 'toHaveSpyInteractions', 'toMatch', 'toThrow', @@ -6605,6 +6606,41 @@ getJasmineRequireObj().toHaveClass = function(j$) { return toHaveClass; }; +getJasmineRequireObj().toHaveClasses = function(j$) { + /** + * {@link expect} the actual value to be a DOM element that has the expected classes + * @function + * @name matchers#toHaveClasses + * @since 5.6.0 + * @param {Object} expected - The class names to test for + * @example + * const el = document.createElement('div'); + * el.className = 'foo bar baz'; + * expect(el).toHaveClasses(['bar', 'baz']); + */ + function toHaveClasses(matchersUtil) { + return { + compare: function(actual, expected) { + if (!isElement(actual)) { + throw new Error(matchersUtil.pp(actual) + ' is not a DOM element'); + } + + return { + pass: expected.every(e => actual.classList.contains(e)) + }; + } + }; + } + + function isElement(maybeEl) { + return ( + maybeEl && maybeEl.classList && j$.isFunction_(maybeEl.classList.contains) + ); + } + + return toHaveClasses; +}; + getJasmineRequireObj().toHaveSize = function(j$) { /** * {@link expect} the actual size to be equal to the expected, using array-like length or object keys size. diff --git a/spec/core/integration/MatchersSpec.js b/spec/core/integration/MatchersSpec.js index f09656cd..51b5e148 100755 --- a/spec/core/integration/MatchersSpec.js +++ b/spec/core/integration/MatchersSpec.js @@ -621,6 +621,20 @@ describe('Matchers (Integration)', function() { }); }); + describe('toHaveClasses', function() { + verifyPasses(function(env) { + const el = specHelpers.domHelpers.createElementWithClassName( + 'foo bar baz' + ); + env.expect(el).toHaveClasses(['bar', 'baz']); + }); + + verifyFails(function(env) { + const el = specHelpers.domHelpers.createElementWithClassName('foo bar'); + env.expect(el).toHaveClasses(['bar', 'baz']); + }); + }); + describe('toHaveSpyInteractions', function() { let spyObj; beforeEach(function() { diff --git a/spec/core/matchers/toHaveClassesSpec.js b/spec/core/matchers/toHaveClassesSpec.js new file mode 100644 index 00000000..0a55edc3 --- /dev/null +++ b/spec/core/matchers/toHaveClassesSpec.js @@ -0,0 +1,48 @@ +describe('toHaveClasses', function() { + it('fails for a DOM element that lacks all the expected classes', function() { + const matcher = jasmineUnderTest.matchers.toHaveClasses(), + result = matcher.compare( + specHelpers.domHelpers.createElementWithClassName(''), + ['foo', 'bar'] + ); + + expect(result.pass).toBe(false); + }); + + it('passes for a DOM element that has all the expected classes', function() { + const matcher = jasmineUnderTest.matchers.toHaveClasses(), + el = specHelpers.domHelpers.createElementWithClassName('foo bar baz'); + + expect(matcher.compare(el, ['foo', 'bar']).pass).toBe(true); + }); + + it('fails for a DOM element that only has some matching classes', function() { + const matcher = jasmineUnderTest.matchers.toHaveClasses(), + el = specHelpers.domHelpers.createElementWithClassName('foo bar'); + + expect(matcher.compare(el, ['foo', 'can']).pass).toBe(false); + }); + + it('throws an exception when actual is not a DOM element', function() { + const matcher = jasmineUnderTest.matchers.toHaveClasses({ + pp: jasmineUnderTest.makePrettyPrinter() + }); + + expect(function() { + matcher.compare('x', ['foo']); + }).toThrowError("'x' is not a DOM element"); + + expect(function() { + matcher.compare(undefined, ['foo']); + }).toThrowError('undefined is not a DOM element'); + + const textNode = specHelpers.domHelpers.document.createTextNode(''); + expect(function() { + matcher.compare(textNode, ['foo']); + }).toThrowError('HTMLNode is not a DOM element'); + + expect(function() { + matcher.compare({ classList: '' }, ['foo']); + }).toThrowError("Object({ classList: '' }) is not a DOM element"); + }); +}); diff --git a/src/core/matchers/requireMatchers.js b/src/core/matchers/requireMatchers.js index b15435c4..c8fb4de3 100755 --- a/src/core/matchers/requireMatchers.js +++ b/src/core/matchers/requireMatchers.js @@ -27,6 +27,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) { 'toHaveBeenCalledTimes', 'toHaveBeenCalledWith', 'toHaveClass', + 'toHaveClasses', 'toHaveSpyInteractions', 'toMatch', 'toThrow', diff --git a/src/core/matchers/toHaveClasses.js b/src/core/matchers/toHaveClasses.js new file mode 100644 index 00000000..e8275b35 --- /dev/null +++ b/src/core/matchers/toHaveClasses.js @@ -0,0 +1,34 @@ +getJasmineRequireObj().toHaveClasses = function(j$) { + /** + * {@link expect} the actual value to be a DOM element that has the expected classes + * @function + * @name matchers#toHaveClasses + * @since 5.6.0 + * @param {Object} expected - The class names to test for + * @example + * const el = document.createElement('div'); + * el.className = 'foo bar baz'; + * expect(el).toHaveClasses(['bar', 'baz']); + */ + function toHaveClasses(matchersUtil) { + return { + compare: function(actual, expected) { + if (!isElement(actual)) { + throw new Error(matchersUtil.pp(actual) + ' is not a DOM element'); + } + + return { + pass: expected.every(e => actual.classList.contains(e)) + }; + } + }; + } + + function isElement(maybeEl) { + return ( + maybeEl && maybeEl.classList && j$.isFunction_(maybeEl.classList.contains) + ); + } + + return toHaveClasses; +};