Skip to content

Commit 55957a4

Browse files
committed
add inverse expect helpers
1 parent af19110 commit 55957a4

File tree

6 files changed

+212
-14
lines changed

6 files changed

+212
-14
lines changed

docs/ExpectAPI.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ describe('Beware of a misunderstanding! A sequence of dice rolls', () => {
223223
});
224224
```
225225

226+
### `expect.arrayNotContaining(array)`
227+
228+
`expect.arrayNotContaining(array)` matches a received array which contains none of
229+
the elements in the expected array. That is, the expected array **is not a subset**
230+
of the received array.
231+
232+
It is the inverse of `expect.arrayContaining`.
233+
226234
### `expect.assertions(number)`
227235

228236
`expect.assertions(number)` verifies that a certain number of assertions are
@@ -273,6 +281,7 @@ test('prepareState prepares a valid state', () => {
273281
The `expect.hasAssertions()` call ensures that the `prepareState` callback
274282
actually gets called.
275283

284+
276285
### `expect.objectContaining(object)`
277286

278287
`expect.objectContaining(object)` matches any received object that recursively
@@ -300,13 +309,27 @@ test('onPress gets called with the right thing', () => {
300309
});
301310
```
302311

303-
### `expect.stringContaining(string)`
312+
### `expect.objectNotContaining(object)`
313+
314+
`expect.objectNotContaining(object)` matches any received object that does not recursively
315+
match the expected properties. That is, the expected object **is not a subset** of
316+
the received object. Therefore, it matches a received object which contains
317+
properties that are **not** in the expected object.
318+
319+
It is the inverse of `expect.objectContaining`.
304320

305-
##### available in Jest **19.0.0+**
321+
### `expect.stringContaining(string)`
306322

307323
`expect.stringContaining(string)` matches any received string that contains the
308324
exact expected string.
309325

326+
### `expect.stringNotContaining(string)`
327+
328+
`expect.stringNotContaining(string)` matches any received string that does not contain the
329+
exact expected string.
330+
331+
It is the inverse of `expect.stringContaining`.
332+
310333
### `expect.stringMatching(regexp)`
311334

312335
`expect.stringMatching(regexp)` matches any received string that matches the
@@ -340,6 +363,13 @@ describe('stringMatching in arrayContaining', () => {
340363
});
341364
```
342365

366+
### `expect.stringNotMatching(regexp)`
367+
368+
`expect.stringNotMatching(regexp)` matches any received string that does not match the
369+
expected regexp.
370+
371+
It is the inverse of `expect.stringMatching`.
372+
343373
### `expect.addSnapshotSerializer(serializer)`
344374

345375
You can call `expect.addSnapshotSerializer` to add a module that formats

packages/expect/src/__tests__/asymmetric_matchers.test.js

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ const {
1313
any,
1414
anything,
1515
arrayContaining,
16+
arrayNotContaining,
1617
objectContaining,
18+
objectNotContaining,
1719
stringContaining,
20+
stringNotContaining,
1821
stringMatching,
22+
stringNotMatching,
1923
} = require('../asymmetric_matchers');
2024

2125
test('Any.asymmetricMatch()', () => {
@@ -55,15 +59,6 @@ test('Anything matches any type', () => {
5559
});
5660
});
5761

58-
test('Anything does not match null and undefined', () => {
59-
[
60-
anything().asymmetricMatch(null),
61-
anything().asymmetricMatch(undefined),
62-
].forEach(test => {
63-
jestExpect(test).toBe(false);
64-
});
65-
});
66-
6762
test('Anything.toAsymmetricMatcher()', () => {
6863
jestExpect(anything().toAsymmetricMatcher()).toBe('Anything');
6964
});
@@ -89,6 +84,27 @@ test('ArrayContaining throws for non-arrays', () => {
8984
}).toThrow();
9085
});
9186

87+
test('ArrayNotContaining matches', () => {
88+
jestExpect(arrayNotContaining(['foo']).asymmetricMatch(['bar'])).toBe(true);
89+
});
90+
91+
test('ArrayNotContaining does not match', () => {
92+
[
93+
arrayNotContaining([]).asymmetricMatch('jest'),
94+
arrayNotContaining(['foo']).asymmetricMatch(['foo']),
95+
arrayNotContaining(['foo']).asymmetricMatch(['foo', 'bar']),
96+
arrayNotContaining([]).asymmetricMatch({}),
97+
].forEach(test => {
98+
jestExpect(test).toEqual(false);
99+
});
100+
});
101+
102+
test('ArrayNotContaining throws for non-arrays', () => {
103+
jestExpect(() => {
104+
arrayNotContaining('foo').asymmetricMatch([]);
105+
}).toThrow();
106+
});
107+
92108
test('ObjectContaining matches', () => {
93109
[
94110
objectContaining({}).asymmetricMatch('jest'),
@@ -139,6 +155,36 @@ test('ObjectContaining throws for non-objects', () => {
139155
jestExpect(() => objectContaining(1337).asymmetricMatch()).toThrow();
140156
});
141157

158+
test('ObjectNotContaining matches', () => {
159+
[
160+
objectNotContaining({}).asymmetricMatch('jest'),
161+
objectNotContaining({foo: 'foo'}).asymmetricMatch({bar: 'bar'}),
162+
objectNotContaining({foo: 'foo'}).asymmetricMatch({foo: 'foox'}),
163+
objectNotContaining({foo: undefined}).asymmetricMatch({}),
164+
].forEach(test => {
165+
jestExpect(test).toEqual(true);
166+
});
167+
});
168+
169+
test('ObjectNotContaining does not match', () => {
170+
[
171+
objectNotContaining({foo: 'foo'}).asymmetricMatch({
172+
foo: 'foo',
173+
jest: 'jest',
174+
}),
175+
objectNotContaining({foo: undefined}).asymmetricMatch({foo: undefined}),
176+
objectNotContaining({
177+
first: objectNotContaining({second: {}}),
178+
}).asymmetricMatch({first: {second: {}}}),
179+
].forEach(test => {
180+
jestExpect(test).toEqual(false);
181+
});
182+
});
183+
184+
test('ObjectNotContaining throws for non-objects', () => {
185+
jestExpect(() => objectNotContaining(1337).asymmetricMatch()).toThrow();
186+
});
187+
142188
test('StringContaining matches string against string', () => {
143189
jestExpect(stringContaining('en*').asymmetricMatch('queen*')).toBe(true);
144190
jestExpect(stringContaining('en').asymmetricMatch('queue')).toBe(false);
@@ -151,6 +197,18 @@ test('StringContaining throws for non-strings', () => {
151197
}).toThrow();
152198
});
153199

200+
test('StringNotContaining matches string against string', () => {
201+
jestExpect(stringNotContaining('en*').asymmetricMatch('queen*')).toBe(false);
202+
jestExpect(stringNotContaining('en').asymmetricMatch('queue')).toBe(true);
203+
jestExpect(stringNotContaining('en').asymmetricMatch({})).toBe(true);
204+
});
205+
206+
test('StringNotContaining throws for non-strings', () => {
207+
jestExpect(() => {
208+
stringNotContaining([1]).asymmetricMatch('queen');
209+
}).toThrow();
210+
});
211+
154212
test('StringMatching matches string against regexp', () => {
155213
jestExpect(stringMatching(/en/).asymmetricMatch('queen')).toBe(true);
156214
jestExpect(stringMatching(/en/).asymmetricMatch('queue')).toBe(false);
@@ -168,3 +226,21 @@ test('StringMatching throws for non-strings and non-regexps', () => {
168226
stringMatching([1]).asymmetricMatch('queen');
169227
}).toThrow();
170228
});
229+
230+
test('StringNotMatching matches string against regexp', () => {
231+
jestExpect(stringNotMatching(/en/).asymmetricMatch('queen')).toBe(false);
232+
jestExpect(stringNotMatching(/en/).asymmetricMatch('queue')).toBe(true);
233+
jestExpect(stringNotMatching(/en/).asymmetricMatch({})).toBe(true);
234+
});
235+
236+
test('StringNotMatching matches string against string', () => {
237+
jestExpect(stringNotMatching('en').asymmetricMatch('queen')).toBe(false);
238+
jestExpect(stringNotMatching('en').asymmetricMatch('queue')).toBe(true);
239+
jestExpect(stringNotMatching('en').asymmetricMatch({})).toBe(true);
240+
});
241+
242+
test('StringNotMatching throws for non-strings and non-regexps', () => {
243+
jestExpect(() => {
244+
stringNotMatching([1]).asymmetricMatch('queen');
245+
}).toThrow();
246+
});

packages/expect/src/__tests__/utils.test.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
'use strict';
1010

1111
const {stringify} = require('jest-matcher-utils');
12-
const {getObjectSubset, getPath} = require('../utils');
12+
const {emptyObject, getObjectSubset, getPath} = require('../utils');
1313

1414
describe('getPath()', () => {
1515
test('property exists', () => {
@@ -107,3 +107,18 @@ describe('getObjectSubset()', () => {
107107
);
108108
});
109109
});
110+
111+
describe('emptyObject()', () => {
112+
test('matches an empty object', () => {
113+
expect(emptyObject({})).toBe(true);
114+
});
115+
116+
test('does not match an object with keys', () => {
117+
expect(emptyObject({foo: undefined})).toBe(false);
118+
});
119+
120+
test('does not match a non-object', () => {
121+
expect(emptyObject(null)).toBe(false);
122+
expect(emptyObject(34)).toBe(false);
123+
});
124+
});

packages/expect/src/asymmetric_matchers.js

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
isUndefined,
1616
} from './jasmine_utils';
1717

18+
import {emptyObject} from './utils';
19+
1820
class AsymmetricMatcher {
1921
$$typeof: Symbol;
2022

@@ -121,7 +123,7 @@ class ArrayContaining extends AsymmetricMatcher {
121123
asymmetricMatch(other: Array<any>) {
122124
if (!Array.isArray(this.sample)) {
123125
throw new Error(
124-
"You must provide an array to ArrayContaining, not '" +
126+
`You must provide an array to ${this.toString()}, not '` +
125127
typeof this.sample +
126128
"'.",
127129
);
@@ -143,6 +145,16 @@ class ArrayContaining extends AsymmetricMatcher {
143145
}
144146
}
145147

148+
class ArrayNotContaining extends ArrayContaining {
149+
asymmetricMatch(other: Array<any>) {
150+
return !super.asymmetricMatch(other);
151+
}
152+
153+
toString() {
154+
return 'ArrayNotContaining';
155+
}
156+
}
157+
146158
class ObjectContaining extends AsymmetricMatcher {
147159
sample: Object;
148160

@@ -154,7 +166,7 @@ class ObjectContaining extends AsymmetricMatcher {
154166
asymmetricMatch(other: Object) {
155167
if (typeof this.sample !== 'object') {
156168
throw new Error(
157-
"You must provide an object to ObjectContaining, not '" +
169+
`You must provide an object to ${this.toString()}, not '` +
158170
typeof this.sample +
159171
"'.",
160172
);
@@ -181,6 +193,35 @@ class ObjectContaining extends AsymmetricMatcher {
181193
}
182194
}
183195

196+
class ObjectNotContaining extends ObjectContaining {
197+
asymmetricMatch(other: Object) {
198+
if (typeof this.sample !== 'object') {
199+
throw new Error(
200+
`You must provide an object to ${this.toString()}, not '` +
201+
typeof this.sample +
202+
"'.",
203+
);
204+
}
205+
206+
for (const property in this.sample) {
207+
if (
208+
hasProperty(other, property) &&
209+
equals(this.sample[property], other[property]) &&
210+
!emptyObject(this.sample[property]) &&
211+
!emptyObject(other[property])
212+
) {
213+
return false;
214+
}
215+
}
216+
217+
return true;
218+
}
219+
220+
toString() {
221+
return 'ObjectNotContaining';
222+
}
223+
}
224+
184225
class StringContaining extends AsymmetricMatcher {
185226
sample: string;
186227

@@ -209,6 +250,16 @@ class StringContaining extends AsymmetricMatcher {
209250
}
210251
}
211252

253+
class StringNotContaining extends StringContaining {
254+
asymmetricMatch(other: string) {
255+
return !super.asymmetricMatch(other);
256+
}
257+
258+
toString() {
259+
return 'StringNotContaining';
260+
}
261+
}
262+
212263
class StringMatching extends AsymmetricMatcher {
213264
sample: RegExp;
214265

@@ -238,13 +289,27 @@ class StringMatching extends AsymmetricMatcher {
238289
}
239290
}
240291

292+
class StringNotMatching extends StringMatching {
293+
asymmetricMatch(other: string) {
294+
return !super.asymmetricMatch(other);
295+
}
296+
}
297+
241298
export const any = (expectedObject: any) => new Any(expectedObject);
242299
export const anything = () => new Anything();
243300
export const arrayContaining = (sample: Array<any>) =>
244301
new ArrayContaining(sample);
302+
export const arrayNotContaining = (sample: Array<any>) =>
303+
new ArrayNotContaining(sample);
245304
export const objectContaining = (sample: Object) =>
246305
new ObjectContaining(sample);
306+
export const objectNotContaining = (sample: Object) =>
307+
new ObjectNotContaining(sample);
247308
export const stringContaining = (expected: string) =>
248309
new StringContaining(expected);
310+
export const stringNotContaining = (expected: string) =>
311+
new StringNotContaining(expected);
249312
export const stringMatching = (expected: string | RegExp) =>
250313
new StringMatching(expected);
314+
export const stringNotMatching = (expected: string | RegExp) =>
315+
new StringNotMatching(expected);

packages/expect/src/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ import {
2929
any,
3030
anything,
3131
arrayContaining,
32+
arrayNotContaining,
3233
objectContaining,
34+
objectNotContaining,
3335
stringContaining,
36+
stringNotContaining,
3437
stringMatching,
38+
stringNotMatching,
3539
} from './asymmetric_matchers';
3640
import {
3741
INTERNAL_MATCHER_FLAG,
@@ -259,9 +263,13 @@ expect.extend = (matchers: MatchersObject): void =>
259263
expect.anything = anything;
260264
expect.any = any;
261265
expect.objectContaining = objectContaining;
266+
expect.objectNotContaining = objectNotContaining;
262267
expect.arrayContaining = arrayContaining;
268+
expect.arrayNotContaining = arrayNotContaining;
263269
expect.stringContaining = stringContaining;
270+
expect.stringNotContaining = stringNotContaining;
264271
expect.stringMatching = stringMatching;
272+
expect.stringNotMatching = stringNotMatching;
265273

266274
const _validateResult = result => {
267275
if (

packages/expect/src/utils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,7 @@ export const partition = <T>(
195195

196196
return result;
197197
};
198+
199+
export function emptyObject(obj: any) {
200+
return obj && typeof obj === 'object' ? !Object.keys(obj).length : false;
201+
}

0 commit comments

Comments
 (0)