Skip to content

Commit

Permalink
Introduce toStrictEqual (#6032)
Browse files Browse the repository at this point in the history
* Introduce toStrictEqual

* address commments

* docs + prettier

* Update docs

* Also fix the test example

* lint md
  • Loading branch information
emilsjolander authored and cpojer committed Apr 19, 2018
1 parent 440e9c6 commit eaee8a6
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
([#5980](https://github.com/facebook/jest/pull/5980))
* `[jest-message-util]` Include column in stack frames
([#5889](https://github.com/facebook/jest/pull/5889))
* `[expect]` Introduce toStrictEqual
([#6032](https://github.com/facebook/jest/pull/6032))

### Fixes

Expand Down
27 changes: 27 additions & 0 deletions docs/ExpectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,33 @@ from the test.
_Note: While snapshot testing is most commonly used with React components, any
serializable value can be used as a snapshot._

### `.toStrictEqual(value)`

Use `.toStrictEqual` to test that objects have the same types as well as
structure.

Differences from `.toEqual`:

* Keys with `undefined` properties are checked. e.g. `{a: undefined, b: 2}` does
not match `{b: 2}` when using `.toStrictEqual`.
* Object types are checked to be equal. e.g. A class instance with fields `a`
and `b` will not equal a literal object with fields `a` and `b`.

```js
class LaCroix {
constructor(flavor) {
this.flavor = flavor;
}
}

describe('the La Croix cans on my desk', () => {
test('are not semantically the same', () => {
expect(new LaCroix('lemon')).toEqual({flavor: 'lemon'});
expect(new LaCroix('lemon')).not.toStrictEqual({flavor: 'lemon'});
});
});
```

### `.toThrow(error)`

Also under the alias: `.toThrowError(error)`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`escape regex 1`] = `/\\\\dd\\\\ \\\\s\\+ \\\\w \\\\\\\\\\\\\\[ \\\\\\. blahzz\\.\\* \\[xyz\\]\\+/`;

exports[`escape regex nested in object 1`] = `
Object {
"regex": /\\\\dd\\\\ \\\\s\\+ \\\\w \\\\\\\\\\\\\\[ \\\\\\. blahzz\\.\\* \\[xyz\\]\\+/,
}
`;
28 changes: 28 additions & 0 deletions packages/expect/src/__tests__/matchers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,34 @@ describe('.toBe()', () => {
});
});

describe('.toStrictEqual()', () => {
class TestClass {
constructor(a, b) {
this.a = a;
this.b = b;
}
}

it('does not ignore keys with undefined values', () => {
expect({
a: undefined,
b: 2,
}).not.toStrictEqual({b: 2});
});

it('passes when comparing same type', () => {
expect({
test: new TestClass(1, 2),
}).toStrictEqual({test: new TestClass(1, 2)});
});

it('does not pass for different types', () => {
expect({
test: new TestClass(1, 2),
}).not.toStrictEqual({test: {a: 1, b: 2}});
});
});

describe('.toEqual()', () => {
[
[true, false],
Expand Down
28 changes: 17 additions & 11 deletions packages/expect/src/jasmine_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
type Tester = (a: any, b: any) => boolean | typeof undefined;

// Extracted out of jasmine 2.5.2
export function equals(a: any, b: any, customTesters?: Array<Tester>): boolean {
export function equals(a: any, b: any, customTesters?: Array<Tester>, strictCheck?: boolean): boolean {
customTesters = customTesters || [];
return eq(a, b, [], [], customTesters);
return eq(a, b, [], [], customTesters, strictCheck ? hasKey : hasDefinedKey);
}

function isAsymmetric(obj) {
Expand All @@ -56,7 +56,7 @@ function asymmetricMatch(a, b) {

// Equality function lovingly adapted from isEqual in
// [Underscore](http://underscorejs.org)
function eq(a, b, aStack, bStack, customTesters): boolean {
function eq(a, b, aStack, bStack, customTesters, hasKey): boolean {
var result = true;

var asymmetricResult = asymmetricMatch(a, b);
Expand Down Expand Up @@ -160,28 +160,28 @@ function eq(a, b, aStack, bStack, customTesters): boolean {
}

while (size--) {
result = eq(a[size], b[size], aStack, bStack, customTesters);
result = eq(a[size], b[size], aStack, bStack, customTesters, hasKey);
if (!result) {
return false;
}
}
}

// Deep compare objects.
var aKeys = keys(a, className == '[object Array]'),
var aKeys = keys(a, className == '[object Array]', hasKey),
key;
size = aKeys.length;

// Ensure that both objects contain the same number of properties before comparing deep equality.
if (keys(b, className == '[object Array]').length !== size) {
if (keys(b, className == '[object Array]', hasKey).length !== size) {
return false;
}

while (size--) {
key = aKeys[size];

// Deep compare each member
result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters);
result = hasKey(b, key) && eq(a[key], b[key], aStack, bStack, customTesters, hasKey);

if (!result) {
return false;
Expand All @@ -194,11 +194,11 @@ function eq(a, b, aStack, bStack, customTesters): boolean {
return result;
}

function keys(obj, isArray) {
function keys(obj, isArray, hasKey) {
var allKeys = (function(o) {
var keys = [];
for (var key in o) {
if (has(o, key)) {
if (hasKey(o, key)) {
keys.push(key);
}
}
Expand All @@ -223,9 +223,15 @@ function keys(obj, isArray) {
return extraKeys;
}

function has(obj, key) {
function hasDefinedKey(obj, key) {
return (
Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== undefined
hasKey(obj, key) && obj[key] !== undefined
);
}

function hasKey(obj, key) {
return (
Object.prototype.hasOwnProperty.call(obj, key)
);
}

Expand Down
38 changes: 38 additions & 0 deletions packages/expect/src/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
getPath,
iterableEquality,
subsetEquality,
typeEquality,
} from './utils';
import {equals} from './jasmine_utils';

Expand Down Expand Up @@ -615,6 +616,43 @@ const matchers: MatchersObject = {

return {message, pass};
},

toStrictEqual(received: any, expected: any) {
const pass = equals(
received,
expected,
[iterableEquality, typeEquality],
true,
);

const message = pass
? () =>
matcherHint('.not.toStrictEqual') +
'\n\n' +
`Expected value to not equal:\n` +
` ${printExpected(expected)}\n` +
`Received:\n` +
` ${printReceived(received)}`
: () => {
const diffString = diff(expected, received, {
expand: this.expand,
});
return (
matcherHint('.toStrictEqual') +
'\n\n' +
`Expected value to equal:\n` +
` ${printExpected(expected)}\n` +
`Received:\n` +
` ${printReceived(received)}` +
(diffString ? `\n\nDifference:\n\n${diffString}` : '')
);
};

// Passing the the actual and expected objects so that a custom reporter
// could access them, for example in order to display a custom visual diff,
// or create a different error message
return {actual: received, expected, message, name: 'toStrictEqual', pass};
},
};

export default matchers;
8 changes: 8 additions & 0 deletions packages/expect/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ export const subsetEquality = (object: Object, subset: Object) => {
);
};

export const typeEquality = (a: any, b: any) => {
if (a == null || b == null || a.constructor.name === b.constructor.name) {
return undefined;
}

return false;
};

export const partition = <T>(
items: Array<T>,
predicate: T => boolean,
Expand Down

0 comments on commit eaee8a6

Please sign in to comment.