Skip to content

Commit

Permalink
add .toBeJsonMatching(expectation) matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
Belema committed Apr 20, 2023
1 parent 7b4a92f commit 66acafa
Show file tree
Hide file tree
Showing 10 changed files with 523 additions and 8 deletions.
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
25 changes: 25 additions & 0 deletions src/matchers/toBeJsonMatching.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { matchesObject, tryParseJSON } from '../utils';

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

const parsed = tryParseJSON(actual);
const isValidJSON = typeof parsed !== 'undefined';

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

const failMessage =
`${matcherHint('.toBeJsonMatching')}\n\n` +
`Expected input to be a JSON string containing:\n ${printExpected(expected)}\n` +
`${isValidJSON ? `Received:\n ${printReceived(parsed)}` : `Received invalid JSON:\n ${printReceived(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,14 +1,9 @@
import { containsEntry } from '../utils';
import { partiallyContains } from '../utils';

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

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,
Expand Down
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 66acafa

Please sign in to comment.