Skip to content

Commit

Permalink
feat: add .toBeJsonMatching(expectation) matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
Belema committed Aug 2, 2022
1 parent 552e950 commit 2dede58
Show file tree
Hide file tree
Showing 9 changed files with 500 additions and 8 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ If you've come here to help contribute - Thanks! Take a look at the [contributin
- [.toBeString()](#tobestring)
- [.toBeHexadecimal(string)](#tobehexadecimal)
- [.toBeDateString(string)](#tobedatestringstring)
- [.toBeJsonMatching(expectation)](#tobejsonmatchingexpectation)
- [.toEqualCaseInsensitive(string)](#toequalcaseinsensitivestring)
- [.toStartWith(prefix)](#tostartwithprefix)
- [.toEndWith(suffix)](#toendwithsuffix)
Expand Down Expand Up @@ -1029,6 +1030,28 @@ test('passes when value is a valid toBeDateString', () => {
});
```
### .toBeJsonMatching(expectation)
Use `.toBeJsonMatching` to check that a string is the JSON representation of a JavaScript object that matches a subset of the properties of the expectation. It will match received JSON representations of objects with properties that are **not** in the expectation.
```js
test('passes when given JSON string matches expectation', () => {
const data = JSON.stringify({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'bar', h: 'baz' }] });

expect(data).toBeJsonMatching({});
expect(data).toBeJsonMatching({ a: 42 });
expect(data).toBeJsonMatching({ a: 42, b: 'foo' });
expect(data).toBeJsonMatching({ a: 42, b: 'foo', c: {} });
expect(data).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello' } });
expect(data).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' } });
expect(data).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [] });
expect(data).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7] });
expect(data).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, {}] });
expect(data).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'bar' }] });
expect(data).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'bar', h: 'baz' }] });
});
```
#### .toEqualCaseInsensitive(string)
Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings.
Expand Down
1 change: 1 addition & 0 deletions src/matchers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { toBeFrozen } from './toBeFrozen';
export { toBeFunction } from './toBeFunction';
export { toBeHexadecimal } from './toBeHexadecimal';
export { toBeInteger } from './toBeInteger';
export { toBeJsonMatching } from './toBeJsonMatching';
export { toBeNaN } from './toBeNaN';
export { toBeNegative } from './toBeNegative';
export { toBeNil } from './toBeNil';
Expand Down
28 changes: 28 additions & 0 deletions src/matchers/toBeJsonMatching.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { matchesObject, tryParseJSON } from '../utils';

export function toBeJsonMatching(actual, expected) {
const { printExpected, printReceived, matcherHint } = this.utils;

const passMessage =
matcherHint('.not.toBeJsonMatching') +
'\n\n' +
'Expected input to not be a JSON string containing:\n' +
` ${printExpected(expected)}\n` +
'Received:\n' +
` ${printReceived(typeof tryParseJSON(actual) !== 'undefined' ? tryParseJSON(actual) : actual)}`;

const failMessage =
matcherHint('.toBeJsonMatching') +
'\n\n' +
'Expected input to be a JSON string containing:\n' +
` ${printExpected(expected)}\n` +
'Received:\n' +
` ${printReceived(typeof tryParseJSON(actual) !== 'undefined' ? tryParseJSON(actual) : actual)}`;

const pass =
typeof actual === 'string' &&
typeof tryParseJSON(actual) !== 'undefined' &&
matchesObject(this.equals, tryParseJSON(actual), expected);

return { pass, message: () => (pass ? passMessage : failMessage) };
}
9 changes: 2 additions & 7 deletions src/matchers/toPartiallyContain.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { containsEntry } from '../utils';
import { partiallyContains } from '../utils';

export function toPartiallyContain(actual, expected) {
const { printReceived, printExpected, matcherHint } = this.utils;
Expand All @@ -19,12 +19,7 @@ export function toPartiallyContain(actual, expected) {
'Received:\n' +
` ${printReceived(actual)}`;

const pass =
Array.isArray(actual) &&
Array.isArray([expected]) &&
[expected].every(partial =>
actual.some(value => Object.entries(partial).every(entry => containsEntry(this.equals, value, entry))),
);
const pass = partiallyContains(this.equals, actual, [expected]);

return { pass, message: () => (pass ? passMessage : failMessage) };
}
47 changes: 47 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,50 @@ export const isJestMockOrSpy = value => {

export const containsEntry = (equals, obj, [key, value]) =>
obj.hasOwnProperty && Object.prototype.hasOwnProperty.call(obj, key) && equals(obj[key], value);

export const partiallyContains = (equals, actual, expected) =>
Array.isArray(actual) &&
Array.isArray(expected) &&
expected.every(partial =>
actual.some(value => {
if (typeof partial !== 'object' || partial === null) {
return equals(value, partial);
}
if (Array.isArray(partial)) {
return partiallyContains(equals, value, partial);
}
return Object.entries(partial).every(entry => containsEntry(equals, value, entry));
}),
);

export const matchesObject = (equals, actual, expected) => {
if (equals(actual, expected)) {
return true;
}
if (Array.isArray(actual) || Array.isArray(expected)) {
return partiallyContains(equals, actual, expected);
}
if (typeof actual === 'object' && typeof expected === 'object' && expected !== null) {
return Object.getOwnPropertyNames(expected).every(name => {
if (equals(actual[name], expected[name])) {
return true;
}
if (Array.isArray(actual[name]) || Array.isArray(expected[name])) {
return partiallyContains(equals, actual[name], expected[name]);
}
if (typeof actual[name] === 'object' && typeof expected[name] === 'object' && expected[name] !== null) {
return matchesObject(equals, actual[name], expected[name]);
}
return false;
});
}
return false;
};

export const tryParseJSON = input => {
try {
return JSON.parse(input);
} catch {
return undefined;
}
};
Loading

0 comments on commit 2dede58

Please sign in to comment.