diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index f595c1d06aa6..9b9c2c0635c9 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -223,6 +223,14 @@ describe('Beware of a misunderstanding! A sequence of dice rolls', () => { }); ``` +### `expect.arrayNotContaining(array)` + +`expect.arrayNotContaining(array)` matches a received array which contains none of +the elements in the expected array. That is, the expected array **is not a subset** +of the received array. + +It is the inverse of `expect.arrayContaining`. + ### `expect.assertions(number)` `expect.assertions(number)` verifies that a certain number of assertions are @@ -273,6 +281,7 @@ test('prepareState prepares a valid state', () => { The `expect.hasAssertions()` call ensures that the `prepareState` callback actually gets called. + ### `expect.objectContaining(object)` `expect.objectContaining(object)` matches any received object that recursively @@ -300,13 +309,27 @@ test('onPress gets called with the right thing', () => { }); ``` -### `expect.stringContaining(string)` +### `expect.objectNotContaining(object)` + +`expect.objectNotContaining(object)` matches any received object that does not recursively +match the expected properties. That is, the expected object **is not a subset** of +the received object. Therefore, it matches a received object which contains +properties that are **not** in the expected object. + +It is the inverse of `expect.objectContaining`. -##### available in Jest **19.0.0+** +### `expect.stringContaining(string)` `expect.stringContaining(string)` matches any received string that contains the exact expected string. +### `expect.stringNotContaining(string)` + +`expect.stringNotContaining(string)` matches any received string that does not contain the +exact expected string. + +It is the inverse of `expect.stringContaining`. + ### `expect.stringMatching(regexp)` `expect.stringMatching(regexp)` matches any received string that matches the @@ -340,6 +363,13 @@ describe('stringMatching in arrayContaining', () => { }); ``` +### `expect.stringNotMatching(regexp)` + +`expect.stringNotMatching(regexp)` matches any received string that does not match the +expected regexp. + +It is the inverse of `expect.stringMatching`. + ### `expect.addSnapshotSerializer(serializer)` You can call `expect.addSnapshotSerializer` to add a module that formats diff --git a/packages/expect/src/__tests__/asymmetric_matchers.test.js b/packages/expect/src/__tests__/asymmetric_matchers.test.js index 54a3760b7c76..7c233732185f 100644 --- a/packages/expect/src/__tests__/asymmetric_matchers.test.js +++ b/packages/expect/src/__tests__/asymmetric_matchers.test.js @@ -13,9 +13,13 @@ const { any, anything, arrayContaining, + arrayNotContaining, objectContaining, + objectNotContaining, stringContaining, + stringNotContaining, stringMatching, + stringNotMatching, } = require('../asymmetric_matchers'); test('Any.asymmetricMatch()', () => { @@ -55,15 +59,6 @@ test('Anything matches any type', () => { }); }); -test('Anything does not match null and undefined', () => { - [ - anything().asymmetricMatch(null), - anything().asymmetricMatch(undefined), - ].forEach(test => { - jestExpect(test).toBe(false); - }); -}); - test('Anything.toAsymmetricMatcher()', () => { jestExpect(anything().toAsymmetricMatcher()).toBe('Anything'); }); @@ -89,6 +84,27 @@ test('ArrayContaining throws for non-arrays', () => { }).toThrow(); }); +test('ArrayNotContaining matches', () => { + jestExpect(arrayNotContaining(['foo']).asymmetricMatch(['bar'])).toBe(true); +}); + +test('ArrayNotContaining does not match', () => { + [ + arrayNotContaining([]).asymmetricMatch('jest'), + arrayNotContaining(['foo']).asymmetricMatch(['foo']), + arrayNotContaining(['foo']).asymmetricMatch(['foo', 'bar']), + arrayNotContaining([]).asymmetricMatch({}), + ].forEach(test => { + jestExpect(test).toEqual(false); + }); +}); + +test('ArrayNotContaining throws for non-arrays', () => { + jestExpect(() => { + arrayNotContaining('foo').asymmetricMatch([]); + }).toThrow(); +}); + test('ObjectContaining matches', () => { [ objectContaining({}).asymmetricMatch('jest'), @@ -139,6 +155,36 @@ test('ObjectContaining throws for non-objects', () => { jestExpect(() => objectContaining(1337).asymmetricMatch()).toThrow(); }); +test('ObjectNotContaining matches', () => { + [ + objectNotContaining({}).asymmetricMatch('jest'), + objectNotContaining({foo: 'foo'}).asymmetricMatch({bar: 'bar'}), + objectNotContaining({foo: 'foo'}).asymmetricMatch({foo: 'foox'}), + objectNotContaining({foo: undefined}).asymmetricMatch({}), + ].forEach(test => { + jestExpect(test).toEqual(true); + }); +}); + +test('ObjectNotContaining does not match', () => { + [ + objectNotContaining({foo: 'foo'}).asymmetricMatch({ + foo: 'foo', + jest: 'jest', + }), + objectNotContaining({foo: undefined}).asymmetricMatch({foo: undefined}), + objectNotContaining({ + first: objectNotContaining({second: {}}), + }).asymmetricMatch({first: {second: {}}}), + ].forEach(test => { + jestExpect(test).toEqual(false); + }); +}); + +test('ObjectNotContaining throws for non-objects', () => { + jestExpect(() => objectNotContaining(1337).asymmetricMatch()).toThrow(); +}); + test('StringContaining matches string against string', () => { jestExpect(stringContaining('en*').asymmetricMatch('queen*')).toBe(true); jestExpect(stringContaining('en').asymmetricMatch('queue')).toBe(false); @@ -151,6 +197,18 @@ test('StringContaining throws for non-strings', () => { }).toThrow(); }); +test('StringNotContaining matches string against string', () => { + jestExpect(stringNotContaining('en*').asymmetricMatch('queen*')).toBe(false); + jestExpect(stringNotContaining('en').asymmetricMatch('queue')).toBe(true); + jestExpect(stringNotContaining('en').asymmetricMatch({})).toBe(true); +}); + +test('StringNotContaining throws for non-strings', () => { + jestExpect(() => { + stringNotContaining([1]).asymmetricMatch('queen'); + }).toThrow(); +}); + test('StringMatching matches string against regexp', () => { jestExpect(stringMatching(/en/).asymmetricMatch('queen')).toBe(true); jestExpect(stringMatching(/en/).asymmetricMatch('queue')).toBe(false); @@ -168,3 +226,21 @@ test('StringMatching throws for non-strings and non-regexps', () => { stringMatching([1]).asymmetricMatch('queen'); }).toThrow(); }); + +test('StringNotMatching matches string against regexp', () => { + jestExpect(stringNotMatching(/en/).asymmetricMatch('queen')).toBe(false); + jestExpect(stringNotMatching(/en/).asymmetricMatch('queue')).toBe(true); + jestExpect(stringNotMatching(/en/).asymmetricMatch({})).toBe(true); +}); + +test('StringNotMatching matches string against string', () => { + jestExpect(stringNotMatching('en').asymmetricMatch('queen')).toBe(false); + jestExpect(stringNotMatching('en').asymmetricMatch('queue')).toBe(true); + jestExpect(stringNotMatching('en').asymmetricMatch({})).toBe(true); +}); + +test('StringNotMatching throws for non-strings and non-regexps', () => { + jestExpect(() => { + stringNotMatching([1]).asymmetricMatch('queen'); + }).toThrow(); +}); diff --git a/packages/expect/src/__tests__/utils.test.js b/packages/expect/src/__tests__/utils.test.js index 9e850b325a70..95f5d796a1cc 100644 --- a/packages/expect/src/__tests__/utils.test.js +++ b/packages/expect/src/__tests__/utils.test.js @@ -9,7 +9,7 @@ 'use strict'; const {stringify} = require('jest-matcher-utils'); -const {getObjectSubset, getPath} = require('../utils'); +const {emptyObject, getObjectSubset, getPath} = require('../utils'); describe('getPath()', () => { test('property exists', () => { @@ -107,3 +107,18 @@ describe('getObjectSubset()', () => { ); }); }); + +describe('emptyObject()', () => { + test('matches an empty object', () => { + expect(emptyObject({})).toBe(true); + }); + + test('does not match an object with keys', () => { + expect(emptyObject({foo: undefined})).toBe(false); + }); + + test('does not match a non-object', () => { + expect(emptyObject(null)).toBe(false); + expect(emptyObject(34)).toBe(false); + }); +}); diff --git a/packages/expect/src/asymmetric_matchers.js b/packages/expect/src/asymmetric_matchers.js index 717d5f552c05..4945ef6668aa 100644 --- a/packages/expect/src/asymmetric_matchers.js +++ b/packages/expect/src/asymmetric_matchers.js @@ -15,6 +15,8 @@ import { isUndefined, } from './jasmine_utils'; +import {emptyObject} from './utils'; + class AsymmetricMatcher { $$typeof: Symbol; @@ -121,7 +123,7 @@ class ArrayContaining extends AsymmetricMatcher { asymmetricMatch(other: Array) { if (!Array.isArray(this.sample)) { throw new Error( - "You must provide an array to ArrayContaining, not '" + + `You must provide an array to ${this.toString()}, not '` + typeof this.sample + "'.", ); @@ -143,6 +145,16 @@ class ArrayContaining extends AsymmetricMatcher { } } +class ArrayNotContaining extends ArrayContaining { + asymmetricMatch(other: Array) { + return !super.asymmetricMatch(other); + } + + toString() { + return 'ArrayNotContaining'; + } +} + class ObjectContaining extends AsymmetricMatcher { sample: Object; @@ -154,7 +166,7 @@ class ObjectContaining extends AsymmetricMatcher { asymmetricMatch(other: Object) { if (typeof this.sample !== 'object') { throw new Error( - "You must provide an object to ObjectContaining, not '" + + `You must provide an object to ${this.toString()}, not '` + typeof this.sample + "'.", ); @@ -181,6 +193,35 @@ class ObjectContaining extends AsymmetricMatcher { } } +class ObjectNotContaining extends ObjectContaining { + asymmetricMatch(other: Object) { + if (typeof this.sample !== 'object') { + throw new Error( + `You must provide an object to ${this.toString()}, not '` + + typeof this.sample + + "'.", + ); + } + + for (const property in this.sample) { + if ( + hasProperty(other, property) && + equals(this.sample[property], other[property]) && + !emptyObject(this.sample[property]) && + !emptyObject(other[property]) + ) { + return false; + } + } + + return true; + } + + toString() { + return 'ObjectNotContaining'; + } +} + class StringContaining extends AsymmetricMatcher { sample: string; @@ -209,6 +250,16 @@ class StringContaining extends AsymmetricMatcher { } } +class StringNotContaining extends StringContaining { + asymmetricMatch(other: string) { + return !super.asymmetricMatch(other); + } + + toString() { + return 'StringNotContaining'; + } +} + class StringMatching extends AsymmetricMatcher { sample: RegExp; @@ -238,13 +289,27 @@ class StringMatching extends AsymmetricMatcher { } } +class StringNotMatching extends StringMatching { + asymmetricMatch(other: string) { + return !super.asymmetricMatch(other); + } +} + export const any = (expectedObject: any) => new Any(expectedObject); export const anything = () => new Anything(); export const arrayContaining = (sample: Array) => new ArrayContaining(sample); +export const arrayNotContaining = (sample: Array) => + new ArrayNotContaining(sample); export const objectContaining = (sample: Object) => new ObjectContaining(sample); +export const objectNotContaining = (sample: Object) => + new ObjectNotContaining(sample); export const stringContaining = (expected: string) => new StringContaining(expected); +export const stringNotContaining = (expected: string) => + new StringNotContaining(expected); export const stringMatching = (expected: string | RegExp) => new StringMatching(expected); +export const stringNotMatching = (expected: string | RegExp) => + new StringNotMatching(expected); diff --git a/packages/expect/src/index.js b/packages/expect/src/index.js index 50a495f50332..756e26b85bbf 100644 --- a/packages/expect/src/index.js +++ b/packages/expect/src/index.js @@ -29,9 +29,13 @@ import { any, anything, arrayContaining, + arrayNotContaining, objectContaining, + objectNotContaining, stringContaining, + stringNotContaining, stringMatching, + stringNotMatching, } from './asymmetric_matchers'; import { INTERNAL_MATCHER_FLAG, @@ -259,9 +263,13 @@ expect.extend = (matchers: MatchersObject): void => expect.anything = anything; expect.any = any; expect.objectContaining = objectContaining; +expect.objectNotContaining = objectNotContaining; expect.arrayContaining = arrayContaining; +expect.arrayNotContaining = arrayNotContaining; expect.stringContaining = stringContaining; +expect.stringNotContaining = stringNotContaining; expect.stringMatching = stringMatching; +expect.stringNotMatching = stringNotMatching; const _validateResult = result => { if ( diff --git a/packages/expect/src/utils.js b/packages/expect/src/utils.js index 010f5d9b8d40..0d68354f5c38 100644 --- a/packages/expect/src/utils.js +++ b/packages/expect/src/utils.js @@ -195,3 +195,7 @@ export const partition = ( return result; }; + +export function emptyObject(obj: any) { + return obj && typeof obj === 'object' ? !Object.keys(obj).length : false; +}