From 66acafa8405ea2319ef85e4e23b02f35778bfa09 Mon Sep 17 00:00:00 2001 From: Belema Gancarz Date: Sat, 18 Feb 2023 11:03:41 +0000 Subject: [PATCH] add .toBeJsonMatching(expectation) matcher --- src/matchers/index.js | 1 + src/matchers/toBeJsonMatching.js | 25 ++ src/matchers/toPartiallyContain.js | 9 +- src/utils/index.js | 47 +++ .../toBeJsonMatching.test.js.snap | 307 ++++++++++++++++++ test/matchers/toBeJsonMatching.test.js | 74 +++++ test/utils/index.test.js | 31 +- types/index.d.ts | 14 + website/docs/matchers/String.mdx | 22 ++ website/docs/matchers/index.md | 1 + 10 files changed, 523 insertions(+), 8 deletions(-) create mode 100644 src/matchers/toBeJsonMatching.js create mode 100644 test/matchers/__snapshots__/toBeJsonMatching.test.js.snap create mode 100644 test/matchers/toBeJsonMatching.test.js diff --git a/src/matchers/index.js b/src/matchers/index.js index 514b2b03..6a029904 100644 --- a/src/matchers/index.js +++ b/src/matchers/index.js @@ -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'; diff --git a/src/matchers/toBeJsonMatching.js b/src/matchers/toBeJsonMatching.js new file mode 100644 index 00000000..0d009c14 --- /dev/null +++ b/src/matchers/toBeJsonMatching.js @@ -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) }; +} diff --git a/src/matchers/toPartiallyContain.js b/src/matchers/toPartiallyContain.js index f22b16c8..4a0dafd2 100644 --- a/src/matchers/toPartiallyContain.js +++ b/src/matchers/toPartiallyContain.js @@ -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, diff --git a/src/utils/index.js b/src/utils/index.js index 1d38547b..9fc65cb5 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -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; + } +}; diff --git a/test/matchers/__snapshots__/toBeJsonMatching.test.js.snap b/test/matchers/__snapshots__/toBeJsonMatching.test.js.snap new file mode 100644 index 00000000..dda0f942 --- /dev/null +++ b/test/matchers/__snapshots__/toBeJsonMatching.test.js.snap @@ -0,0 +1,307 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 1`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 2`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 3`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42, "b": "foo"} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 4`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42, "b": "foo", "c": {}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 5`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello"}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 6`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 7`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": []} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 8`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7]} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 9`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {}]} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 10`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar"}]} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.not.toBeJsonMatching fails when given JSON string matches expectation 11`] = ` +"expect(received).not.toBeJsonMatching(expected) + +Expected input to not be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 1`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + null +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 2`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + [] +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 3`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": "42"} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 4`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 41} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 5`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": []} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 6`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": {}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 7`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": null} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 8`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "bar"} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 9`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": 7} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 10`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": []} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 11`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "bonjour"}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 12`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "dolly"}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 13`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": 7}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 14`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": []}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 15`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": {}}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 16`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": null}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 17`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": 7} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 18`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": {}} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 19`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [8]} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 20`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, []]} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 21`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "baz"}]} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when given JSON string does not match expectation 22`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz", "i": "buzz"}]} +Received: + {"a": 42, "b": "foo", "c": {"d": "hello", "e": "world"}, "f": [7, {"g": "bar", "h": "baz"}]}" +`; + +exports[`.toBeJsonMatching fails when the given string is not valid JSON 1`] = ` +"expect(received).toBeJsonMatching(expected) + +Expected input to be a JSON string containing: + "This is not a valid JSON string" +Received invalid JSON: + "This is not a valid JSON string"" +`; diff --git a/test/matchers/toBeJsonMatching.test.js b/test/matchers/toBeJsonMatching.test.js new file mode 100644 index 00000000..54ec4fec --- /dev/null +++ b/test/matchers/toBeJsonMatching.test.js @@ -0,0 +1,74 @@ +import * as matcher from 'src/matchers/toBeJsonMatching'; + +expect.extend(matcher); + +const string = JSON.stringify({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'bar', h: 'baz' }] }); + +const matches = [ + {}, + { a: 42 }, + { a: 42, b: 'foo' }, + { a: 42, b: 'foo', c: {} }, + { a: 42, b: 'foo', c: { d: 'hello' } }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' } }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [] }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7] }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, {}] }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'bar' }] }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'bar', h: 'baz' }] }, +]; + +const nonMatches = [ + null, + [], + { a: '42' }, + { a: 41 }, + { a: [] }, + { a: {} }, + { a: null }, + { a: 42, b: 'bar' }, + { a: 42, b: 'foo', c: 7 }, + { a: 42, b: 'foo', c: [] }, + { a: 42, b: 'foo', c: { d: 'bonjour' } }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'dolly' } }, + { a: 42, b: 'foo', c: { d: 'hello', e: 7 } }, + { a: 42, b: 'foo', c: { d: 'hello', e: [] } }, + { a: 42, b: 'foo', c: { d: 'hello', e: {} } }, + { a: 42, b: 'foo', c: { d: 'hello', e: null } }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: 7 }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: {} }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [8] }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, []] }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'baz' }] }, + { a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'bar', h: 'baz', i: 'buzz' }] }, +]; + +describe('.toBeJsonMatching', () => { + test.each(matches)('passes when given JSON string matches expectation', expectation => { + expect(string).toBeJsonMatching(expectation); + }); + + test.each(nonMatches)('fails when given JSON string does not match expectation', expectation => { + expect(() => expect(string).toBeJsonMatching(expectation)).toThrowErrorMatchingSnapshot(); + }); + + test('fails when the given string is not valid JSON', () => { + const invalid = 'This is not a valid JSON string'; + expect(() => expect(invalid).toBeJsonMatching(invalid)).toThrowErrorMatchingSnapshot(); + }); +}); + +describe('.not.toBeJsonMatching', () => { + test.each(nonMatches)('passes when given JSON string does not match expectation', expectation => { + expect(string).not.toBeJsonMatching(expectation); + }); + + test.each(matches)('fails when given JSON string matches expectation', expectation => { + expect(() => expect(string).not.toBeJsonMatching(expectation)).toThrowErrorMatchingSnapshot(); + }); + + test('passes when the given string is not valid JSON', () => { + const invalid = 'This is not a valid JSON string'; + expect(invalid).not.toBeJsonMatching(invalid); + }); +}); diff --git a/test/utils/index.test.js b/test/utils/index.test.js index 5d55e2f0..775d92d1 100644 --- a/test/utils/index.test.js +++ b/test/utils/index.test.js @@ -1,4 +1,4 @@ -import { contains, determinePropertyMessage, isJestMockOrSpy } from 'src/utils'; +import { contains, determinePropertyMessage, isJestMockOrSpy, tryParseJSON } from 'src/utils'; let equals; @@ -74,4 +74,33 @@ describe('Utils', () => { expect(isJestMockOrSpy(fn)).toBe(false); }); }); + + describe('.tryParseJSON', () => { + test('returns undefined when the input is not valid JSON', () => { + const invalidJson = '

This is not a valid JSON string

'; + + expect(tryParseJSON(invalidJson)).toBeUndefined(); + }); + + test('returns the expected string when the input is a valid JSON string', () => { + const message = 'Hello World!'; + const validJsonString = JSON.stringify(message); + + expect(tryParseJSON(validJsonString)).toBe(message); + }); + + test('returns the expected number when the input is a valid JSON number', () => { + const number = 42; + const validJsonNumber = JSON.stringify(number); + + expect(tryParseJSON(validJsonNumber)).toBe(number); + }); + + test('returns the expected object when the input is a valid JSON object', () => { + const object = { a: 42, b: 'Hello World!' }; + const validJsonObjet = JSON.stringify(object); + + expect(tryParseJSON(validJsonObjet)).toEqual(object); + }); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index af5fd0ee..544471b3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -381,6 +381,13 @@ interface CustomMatchers extends Record { */ toIncludeMultiple(substring: readonly string[]): R; + /** + * 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. + * + * @param {*} expectation + */ + toBeJsonMatching(expectation: E): R; + /** * Use `.toThrowWithMessage` when checking if a callback function throws an error of a given type with a given error message. * @@ -820,6 +827,13 @@ declare namespace jest { */ toIncludeMultiple(substring: readonly string[]): R; + /** + * 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. + * + * @param {*} expectation + */ + toBeJsonMatching(expectation: E): R; + /** * Use `.toThrowWithMessage` when checking if a callback function throws an error of a given type with a given error message. * diff --git a/website/docs/matchers/String.mdx b/website/docs/matchers/String.mdx index e5aed1b9..76269058 100644 --- a/website/docs/matchers/String.mdx +++ b/website/docs/matchers/String.mdx @@ -52,6 +52,28 @@ Use `.toBeDateString` when checking if a value is a valid date string. });`} +### .toBeJsonMatching(expectation) + +Use `.toBeJsonMatching` to check that a string is the JSON representation of an object that matches the expectation object. + + + {`test('passes when given JSON string matches object', () => { + const string = JSON.stringify({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'bar', h: 'baz' }] }); + + expect(string).toBeJsonMatching({}); + expect(string).toBeJsonMatching({ a: 42 }); + expect(string).toBeJsonMatching({ a: 42, b: 'foo' }); + expect(string).toBeJsonMatching({ a: 42, b: 'foo', c: {} }); + expect(string).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello' } }); + expect(string).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' } }); + expect(string).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [] }); + expect(string).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7] }); + expect(string).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, {}] }); + expect(string).toBeJsonMatching({ a: 42, b: 'foo', c: { d: 'hello', e: 'world' }, f: [7, { g: 'bar' }] }); + expect(string).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. diff --git a/website/docs/matchers/index.md b/website/docs/matchers/index.md index 7674400e..90e3a4b0 100644 --- a/website/docs/matchers/index.md +++ b/website/docs/matchers/index.md @@ -94,6 +94,7 @@ sidebar_position: 1 - [.toBeString()](/docs/matchers/string/#tobestring) - [.toBeHexadecimal(string)](/docs/matchers/string/#tobehexadecimal) - [.toBeDateString(string)](/docs/matchers/string/#tobedatestringstring) +- [.toBeJsonMatching(expectation)](/docs/matchers/string/#tobejsonmatchingexpectation) - [.toEqualCaseInsensitive(string)](/docs/matchers/string/#toequalcaseinsensitivestring) - [.toStartWith(prefix)](/docs/matchers/string/#tostartwithprefix) - [.toEndWith(suffix)](/docs/matchers/string/#toendwithsuffix)