Skip to content

Commit 6c45fbc

Browse files
committed
add .toBeJsonMatching(expectation) matcher
1 parent 865e4bb commit 6c45fbc

File tree

10 files changed

+523
-8
lines changed

10 files changed

+523
-8
lines changed

src/matchers/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export { toBeFrozen } from './toBeFrozen';
2020
export { toBeFunction } from './toBeFunction';
2121
export { toBeHexadecimal } from './toBeHexadecimal';
2222
export { toBeInteger } from './toBeInteger';
23+
export { toBeJsonMatching } from './toBeJsonMatching';
2324
export { toBeNaN } from './toBeNaN';
2425
export { toBeNegative } from './toBeNegative';
2526
export { toBeNil } from './toBeNil';

src/matchers/toBeJsonMatching.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { matchesObject, tryParseJSON } from '../utils';
2+
3+
export function toBeJsonMatching(actual, expected) {
4+
const { printExpected, printReceived, matcherHint } = this.utils;
5+
6+
const parsed = tryParseJSON(actual);
7+
const isValidJSON = typeof parsed !== 'undefined';
8+
9+
const passMessage =
10+
`${matcherHint('.not.toBeJsonMatching')}\n\n` +
11+
`Expected input to not be a JSON string containing:\n ${printExpected(expected)}\n` +
12+
`${isValidJSON ? `Received:\n ${printReceived(parsed)}` : `Received invalid JSON:\n ${printReceived(actual)}`}`;
13+
14+
const failMessage =
15+
`${matcherHint('.toBeJsonMatching')}\n\n` +
16+
`Expected input to be a JSON string containing:\n ${printExpected(expected)}\n` +
17+
`${isValidJSON ? `Received:\n ${printReceived(parsed)}` : `Received invalid JSON:\n ${printReceived(actual)}`}`;
18+
19+
const pass =
20+
typeof actual === 'string' &&
21+
typeof tryParseJSON(actual) !== 'undefined' &&
22+
matchesObject(this.equals, tryParseJSON(actual), expected);
23+
24+
return { pass, message: () => (pass ? passMessage : failMessage) };
25+
}

src/matchers/toPartiallyContain.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { containsEntry } from '../utils';
1+
import { partiallyContains } from '../utils';
22

33
export function toPartiallyContain(actual, expected) {
44
const { printReceived, printExpected, matcherHint } = this.utils;
55

6-
const pass =
7-
Array.isArray(actual) &&
8-
Array.isArray([expected]) &&
9-
[expected].every(partial =>
10-
actual.some(value => Object.entries(partial).every(entry => containsEntry(this.equals, value, entry))),
11-
);
6+
const pass = partiallyContains(this.equals, actual, [expected]);
127

138
return {
149
pass,

src/utils/index.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,50 @@ export const isJestMockOrSpy = value => {
1212

1313
export const containsEntry = (equals, obj, [key, value]) =>
1414
obj.hasOwnProperty && Object.prototype.hasOwnProperty.call(obj, key) && equals(obj[key], value);
15+
16+
export const partiallyContains = (equals, actual, expected) =>
17+
Array.isArray(actual) &&
18+
Array.isArray(expected) &&
19+
expected.every(partial =>
20+
actual.some(value => {
21+
if (typeof partial !== 'object' || partial === null) {
22+
return equals(value, partial);
23+
}
24+
if (Array.isArray(partial)) {
25+
return partiallyContains(equals, value, partial);
26+
}
27+
return Object.entries(partial).every(entry => containsEntry(equals, value, entry));
28+
}),
29+
);
30+
31+
export const matchesObject = (equals, actual, expected) => {
32+
if (equals(actual, expected)) {
33+
return true;
34+
}
35+
if (Array.isArray(actual) || Array.isArray(expected)) {
36+
return partiallyContains(equals, actual, expected);
37+
}
38+
if (typeof actual === 'object' && typeof expected === 'object' && expected !== null) {
39+
return Object.getOwnPropertyNames(expected).every(name => {
40+
if (equals(actual[name], expected[name])) {
41+
return true;
42+
}
43+
if (Array.isArray(actual[name]) || Array.isArray(expected[name])) {
44+
return partiallyContains(equals, actual[name], expected[name]);
45+
}
46+
if (typeof actual[name] === 'object' && typeof expected[name] === 'object' && expected[name] !== null) {
47+
return matchesObject(equals, actual[name], expected[name]);
48+
}
49+
return false;
50+
});
51+
}
52+
return false;
53+
};
54+
55+
export const tryParseJSON = input => {
56+
try {
57+
return JSON.parse(input);
58+
} catch {
59+
return undefined;
60+
}
61+
};

0 commit comments

Comments
 (0)