From fa4b53c5236f8bdf82cc9d00563e7dd11bc881ea Mon Sep 17 00:00:00 2001 From: Evan Scott Date: Sat, 10 Feb 2018 18:07:10 -0500 Subject: [PATCH 1/6] add inverse expect helpers --- docs/ExpectAPI.md | 34 ++++++- .../src/__tests__/asymmetric_matchers.test.js | 94 +++++++++++++++++-- packages/expect/src/__tests__/utils.test.js | 17 +++- packages/expect/src/asymmetric_matchers.js | 69 +++++++++++++- packages/expect/src/index.js | 8 ++ packages/expect/src/utils.js | 4 + 6 files changed, 212 insertions(+), 14 deletions(-) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 2d9a3897e4f6..dbf93932f56b 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 51517855b673..271b43237ad0 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 35031a2264a2..12eceffd953f 100644 --- a/packages/expect/src/utils.js +++ b/packages/expect/src/utils.js @@ -209,3 +209,7 @@ export const isError = (value: any) => { return value instanceof Error; } }; + +export function emptyObject(obj: any) { + return obj && typeof obj === 'object' ? !Object.keys(obj).length : false; +} From e81639d46bbb97f276f0a5be1e60521a39e8064c Mon Sep 17 00:00:00 2001 From: Evan Scott Date: Sat, 10 Feb 2018 18:07:42 -0500 Subject: [PATCH 2/6] linting --- .../__tests__/accept-custom-snapshot-name.test.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 integration-tests/toThrowErrorMatchingSnapshot/__tests__/accept-custom-snapshot-name.test.js diff --git a/integration-tests/toThrowErrorMatchingSnapshot/__tests__/accept-custom-snapshot-name.test.js b/integration-tests/toThrowErrorMatchingSnapshot/__tests__/accept-custom-snapshot-name.test.js new file mode 100644 index 000000000000..4f955d35bb88 --- /dev/null +++ b/integration-tests/toThrowErrorMatchingSnapshot/__tests__/accept-custom-snapshot-name.test.js @@ -0,0 +1,5 @@ +test('accepts custom snapshot name', () => { + expect(() => { + throw new Error('apple'); + }).toThrowErrorMatchingSnapshot('custom-name'); +}); From 404478527be1db6c4e7325f0cffac416975207d0 Mon Sep 17 00:00:00 2001 From: Evan Scott Date: Sun, 4 Mar 2018 04:16:51 -0500 Subject: [PATCH 3/6] switch to expect.not.* format, fix string validation in asymmetric matcher --- docs/ExpectAPI.md | 61 ++++---- .../accept-custom-snapshot-name.test.js | 5 - .../src/__tests__/asymmetric_matchers.test.js | 26 +++- packages/expect/src/asymmetric_matchers.js | 136 +++++++----------- packages/expect/src/index.js | 12 +- 5 files changed, 112 insertions(+), 128 deletions(-) delete mode 100644 integration-tests/toThrowErrorMatchingSnapshot/__tests__/accept-custom-snapshot-name.test.js diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index dbf93932f56b..deecef16cac3 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -223,14 +223,6 @@ 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 @@ -281,6 +273,36 @@ test('prepareState prepares a valid state', () => { The `expect.hasAssertions()` call ensures that the `prepareState` callback actually gets called. +### `expect.not.arrayContaining(array)` + +`expect.not.arrayContaining(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.not.objectContaining(object)` + +`expect.not.objectContaining(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`. + +### `expect.not.stringContaining(string)` + +`expect.not.stringContaining(string)` matches any received string that does not contain the +exact expected string. + +It is the inverse of `expect.stringContaining`. + +### `expect.not.stringMatching(regexp)` + +`expect.not.stringMatching(regexp)` matches any received string that does not match the +expected regexp. + +It is the inverse of `expect.stringMatching`. ### `expect.objectContaining(object)` @@ -309,27 +331,11 @@ test('onPress gets called with the right thing', () => { }); ``` -### `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`. - ### `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 @@ -363,13 +369,6 @@ 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/integration-tests/toThrowErrorMatchingSnapshot/__tests__/accept-custom-snapshot-name.test.js b/integration-tests/toThrowErrorMatchingSnapshot/__tests__/accept-custom-snapshot-name.test.js deleted file mode 100644 index 4f955d35bb88..000000000000 --- a/integration-tests/toThrowErrorMatchingSnapshot/__tests__/accept-custom-snapshot-name.test.js +++ /dev/null @@ -1,5 +0,0 @@ -test('accepts custom snapshot name', () => { - expect(() => { - throw new Error('apple'); - }).toThrowErrorMatchingSnapshot('custom-name'); -}); diff --git a/packages/expect/src/__tests__/asymmetric_matchers.test.js b/packages/expect/src/__tests__/asymmetric_matchers.test.js index 7c233732185f..c1601480e46d 100644 --- a/packages/expect/src/__tests__/asymmetric_matchers.test.js +++ b/packages/expect/src/__tests__/asymmetric_matchers.test.js @@ -188,37 +188,41 @@ test('ObjectNotContaining throws for non-objects', () => { test('StringContaining matches string against string', () => { jestExpect(stringContaining('en*').asymmetricMatch('queen*')).toBe(true); jestExpect(stringContaining('en').asymmetricMatch('queue')).toBe(false); - jestExpect(stringContaining('en').asymmetricMatch({})).toBe(false); }); test('StringContaining throws for non-strings', () => { jestExpect(() => { stringContaining([1]).asymmetricMatch('queen'); }).toThrow(); + + jestExpect(() => { + stringContaining('en*').asymmetricMatch(1); + }).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(); + + jestExpect(() => { + stringNotContaining('en*').asymmetricMatch(1); + }).toThrow(); }); test('StringMatching matches string against regexp', () => { jestExpect(stringMatching(/en/).asymmetricMatch('queen')).toBe(true); jestExpect(stringMatching(/en/).asymmetricMatch('queue')).toBe(false); - jestExpect(stringMatching(/en/).asymmetricMatch({})).toBe(false); }); test('StringMatching matches string against string', () => { jestExpect(stringMatching('en').asymmetricMatch('queen')).toBe(true); jestExpect(stringMatching('en').asymmetricMatch('queue')).toBe(false); - jestExpect(stringMatching('en').asymmetricMatch({})).toBe(false); }); test('StringMatching throws for non-strings and non-regexps', () => { @@ -227,16 +231,20 @@ test('StringMatching throws for non-strings and non-regexps', () => { }).toThrow(); }); +test('StringMatching throws for non-string actual values', () => { + jestExpect(() => { + stringMatching('en').asymmetricMatch(1); + }).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', () => { @@ -244,3 +252,9 @@ test('StringNotMatching throws for non-strings and non-regexps', () => { stringNotMatching([1]).asymmetricMatch('queen'); }).toThrow(); }); + +test('StringNotMatching throws for non-string actual values', () => { + jestExpect(() => { + stringNotMatching('en').asymmetricMatch(1); + }).toThrow(); +}); diff --git a/packages/expect/src/asymmetric_matchers.js b/packages/expect/src/asymmetric_matchers.js index 4945ef6668aa..b8e7a9992264 100644 --- a/packages/expect/src/asymmetric_matchers.js +++ b/packages/expect/src/asymmetric_matchers.js @@ -19,6 +19,7 @@ import {emptyObject} from './utils'; class AsymmetricMatcher { $$typeof: Symbol; + inverse: boolean; constructor() { this.$$typeof = Symbol.for('jest.asymmetricMatcher'); @@ -115,9 +116,10 @@ class Anything extends AsymmetricMatcher { class ArrayContaining extends AsymmetricMatcher { sample: Array; - constructor(sample: Array) { + constructor(sample: Array, inverse: boolean = false) { super(); this.sample = sample; + this.inverse = inverse; } asymmetricMatch(other: Array) { @@ -129,15 +131,18 @@ class ArrayContaining extends AsymmetricMatcher { ); } - return ( + const result = this.sample.length === 0 || (Array.isArray(other) && - this.sample.every(item => other.some(another => equals(item, another)))) - ); + this.sample.every(item => + other.some(another => equals(item, another)), + )); + + return this.inverse ? !result : result; } toString() { - return 'ArrayContaining'; + return `Array${this.inverse ? 'Not' : ''}Containing`; } getExpectedType() { @@ -145,22 +150,13 @@ class ArrayContaining extends AsymmetricMatcher { } } -class ArrayNotContaining extends ArrayContaining { - asymmetricMatch(other: Array) { - return !super.asymmetricMatch(other); - } - - toString() { - return 'ArrayNotContaining'; - } -} - class ObjectContaining extends AsymmetricMatcher { sample: Object; - constructor(sample: Object) { + constructor(sample: Object, inverse: boolean = false) { super(); this.sample = sample; + this.inverse = inverse; } asymmetricMatch(other: Object) { @@ -172,20 +168,35 @@ class ObjectContaining extends AsymmetricMatcher { ); } - for (const property in this.sample) { - if ( - !hasProperty(other, property) || - !equals(this.sample[property], other[property]) - ) { - return false; + if (this.inverse) { + 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; + } else { + for (const property in this.sample) { + if ( + !hasProperty(other, property) || + !equals(this.sample[property], other[property]) + ) { + return false; + } } - } - return true; + return true; + } } toString() { - return 'ObjectContaining'; + return `Object${this.inverse ? 'Not' : ''}Containing`; } getExpectedType() { @@ -193,56 +204,30 @@ 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; - constructor(sample: string) { + constructor(sample: string, inverse: boolean = false) { super(); if (!isA('String', sample)) { throw new Error('Expected is not a string'); } this.sample = sample; + this.inverse = inverse; } asymmetricMatch(other: string) { if (!isA('String', other)) { - return false; + throw new Error('Actual is not a string'); } - return other.includes(this.sample); + const result = other.includes(this.sample); + + return this.inverse ? !result : result; } toString() { - return 'StringContaining'; + return `String${this.inverse ? 'Not' : ''}Containing`; } getExpectedType() { @@ -250,38 +235,31 @@ class StringContaining extends AsymmetricMatcher { } } -class StringNotContaining extends StringContaining { - asymmetricMatch(other: string) { - return !super.asymmetricMatch(other); - } - - toString() { - return 'StringNotContaining'; - } -} - class StringMatching extends AsymmetricMatcher { sample: RegExp; - constructor(sample: string | RegExp) { + constructor(sample: string | RegExp, inverse: boolean = false) { super(); if (!isA('String', sample) && !isA('RegExp', sample)) { throw new Error('Expected is not a String or a RegExp'); } this.sample = new RegExp(sample); + this.inverse = inverse; } asymmetricMatch(other: string) { if (!isA('String', other)) { - return false; + throw new Error('Actual is not a string'); } - return this.sample.test(other); + const result = this.sample.test(other); + + return this.inverse ? !result : result; } toString() { - return 'StringMatching'; + return `String${this.inverse ? 'Not' : ''}Matching`; } getExpectedType() { @@ -289,27 +267,21 @@ 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); + new ArrayContaining(sample, true); export const objectContaining = (sample: Object) => new ObjectContaining(sample); export const objectNotContaining = (sample: Object) => - new ObjectNotContaining(sample); + new ObjectContaining(sample, true); export const stringContaining = (expected: string) => new StringContaining(expected); export const stringNotContaining = (expected: string) => - new StringNotContaining(expected); + new StringContaining(expected, true); export const stringMatching = (expected: string | RegExp) => new StringMatching(expected); export const stringNotMatching = (expected: string | RegExp) => - new StringNotMatching(expected); + new StringMatching(expected, true); diff --git a/packages/expect/src/index.js b/packages/expect/src/index.js index 271b43237ad0..c4da96f53352 100644 --- a/packages/expect/src/index.js +++ b/packages/expect/src/index.js @@ -262,14 +262,18 @@ expect.extend = (matchers: MatchersObject): void => expect.anything = anything; expect.any = any; + +expect.not = { + arrayContaining: arrayNotContaining, + objectContaining: objectNotContaining, + stringContaining: stringNotContaining, + stringMatching: stringNotMatching, +}; + 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 ( From bb60ca312b9eeb87c59a3106a0c5ca9c1ec56476 Mon Sep 17 00:00:00 2001 From: Evan Scott Date: Sun, 4 Mar 2018 04:21:04 -0500 Subject: [PATCH 4/6] add back mistakenly removed test --- .../expect/src/__tests__/asymmetric_matchers.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/expect/src/__tests__/asymmetric_matchers.test.js b/packages/expect/src/__tests__/asymmetric_matchers.test.js index c1601480e46d..9b7a030d4d54 100644 --- a/packages/expect/src/__tests__/asymmetric_matchers.test.js +++ b/packages/expect/src/__tests__/asymmetric_matchers.test.js @@ -59,6 +59,15 @@ 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'); }); From 7ed2010079e2073a6afd16a892b671ae2ad70fbe Mon Sep 17 00:00:00 2001 From: Evan Scott Date: Sun, 4 Mar 2018 14:57:20 -0500 Subject: [PATCH 5/6] lint docs --- docs/ExpectAPI.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index deecef16cac3..c2bc17ff81d2 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -275,32 +275,32 @@ actually gets called. ### `expect.not.arrayContaining(array)` -`expect.not.arrayContaining(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. +`expect.not.arrayContaining(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.not.objectContaining(object)` -`expect.not.objectContaining(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. +`expect.not.objectContaining(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`. ### `expect.not.stringContaining(string)` -`expect.not.stringContaining(string)` matches any received string that does not contain the -exact expected string. +`expect.not.stringContaining(string)` matches any received string that does not +contain the exact expected string. It is the inverse of `expect.stringContaining`. ### `expect.not.stringMatching(regexp)` -`expect.not.stringMatching(regexp)` matches any received string that does not match the -expected regexp. +`expect.not.stringMatching(regexp)` matches any received string that does not +match the expected regexp. It is the inverse of `expect.stringMatching`. From 34b78160058aa072cf3763e923c8fd79bd6fa46a Mon Sep 17 00:00:00 2001 From: Evan Scott Date: Sun, 4 Mar 2018 14:58:37 -0500 Subject: [PATCH 6/6] add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091499af1e48..7f20bf41a7f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ promises ([#5670](https://github.com/facebook/jest/pull/5670)) * `[expect]` Add isError to utils ([#5670](https://github.com/facebook/jest/pull/5670)) +* `[expect]` Add inverse matchers (`expect.not.arrayContaining`, etc., + [#5517](https://github.com/facebook/jest/pull/5517)) ### Fixes