Skip to content

Commit

Permalink
add inverse expect helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
quantizor committed Feb 10, 2018
1 parent af19110 commit 55957a4
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 14 deletions.
34 changes: 32 additions & 2 deletions docs/ExpectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
94 changes: 85 additions & 9 deletions packages/expect/src/__tests__/asymmetric_matchers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ const {
any,
anything,
arrayContaining,
arrayNotContaining,
objectContaining,
objectNotContaining,
stringContaining,
stringNotContaining,
stringMatching,
stringNotMatching,
} = require('../asymmetric_matchers');

test('Any.asymmetricMatch()', () => {
Expand Down Expand Up @@ -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');
});
Expand All @@ -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'),
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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();
});
17 changes: 16 additions & 1 deletion packages/expect/src/__tests__/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
69 changes: 67 additions & 2 deletions packages/expect/src/asymmetric_matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
isUndefined,
} from './jasmine_utils';

import {emptyObject} from './utils';

class AsymmetricMatcher {
$$typeof: Symbol;

Expand Down Expand Up @@ -121,7 +123,7 @@ class ArrayContaining extends AsymmetricMatcher {
asymmetricMatch(other: Array<any>) {
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 +
"'.",
);
Expand All @@ -143,6 +145,16 @@ class ArrayContaining extends AsymmetricMatcher {
}
}

class ArrayNotContaining extends ArrayContaining {
asymmetricMatch(other: Array<any>) {
return !super.asymmetricMatch(other);
}

toString() {
return 'ArrayNotContaining';
}
}

class ObjectContaining extends AsymmetricMatcher {
sample: Object;

Expand All @@ -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 +
"'.",
);
Expand All @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<any>) =>
new ArrayContaining(sample);
export const arrayNotContaining = (sample: Array<any>) =>
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);
8 changes: 8 additions & 0 deletions packages/expect/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ import {
any,
anything,
arrayContaining,
arrayNotContaining,
objectContaining,
objectNotContaining,
stringContaining,
stringNotContaining,
stringMatching,
stringNotMatching,
} from './asymmetric_matchers';
import {
INTERNAL_MATCHER_FLAG,
Expand Down Expand Up @@ -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 (
Expand Down
4 changes: 4 additions & 0 deletions packages/expect/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,7 @@ export const partition = <T>(

return result;
};

export function emptyObject(obj: any) {
return obj && typeof obj === 'object' ? !Object.keys(obj).length : false;
}

0 comments on commit 55957a4

Please sign in to comment.