diff --git a/README.md b/README.md index 6e6cf47..b7ce3a5 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Additional expect matchers for http clients (e.g. Axios), supports `jest`, `vite - [.toHaveNetworkAuthenticationRequiredStatus()](#tohavenetworkauthenticationrequiredstatus) - [.toHaveHeader(`
`[, `
`])](#tohaveheaderheader-name-header-value) - [.toHaveBodyEquals(``)](#tohavebodyequalsbody) + - [.toHaveBodyMatchObject(``)](#tohavebodymatchobjectbody) ## Installation @@ -1359,8 +1360,8 @@ Use `.toHaveBodyEquals` when checking if response body is equal to the expected ```js test('passes when response body match the expected body', async () => { - const response = await axios.get('https://httpstat.us/200'); - expect(response).toHaveBodyEquals('200 OK'); + const response = await axios.get('https://httpstat.us/200'); + expect(response).toHaveBodyEquals('200 OK'); }); test('passes when using .not.toHaveBodyEquals() with different body', async () => { @@ -1369,6 +1370,31 @@ test('passes when using .not.toHaveBodyEquals() with different body', async () = }); ``` +#### .toHaveBodyMatchObject(``) + +Use `.toHaveBodyMatchObject` when checking if response body match to the expected body + +The implementation of the matcher is similar to the implementation of [expect's toMatchObject](https://jestjs.io/docs/en/expect#tomatchobjectobject) +except only valid JSON values or asymmetric matchers are supported in the expected body. + +`undefined` values in the expected body means that the response body should not contain the key at all (not even with null value) + +```js +test('passes when response body match the expected body', async () => { + const response = await axios.get('https://some-api.com'); + expect(response).toHaveBodyMatchObject({ + name: 'John Doe', + }); +}); + +test('passes when using .not.toHaveBodyMatchObject() with different body', async () => { + const response = await axios.get('https://some-api.com'); + expect(response).not.toHaveBodyMatchObject({ + name: 'hello', + }); +}); +``` + ## LICENSE diff --git a/src/matchers/data/index.js b/src/matchers/data/index.js index 83c283e..5c70fc6 100644 --- a/src/matchers/data/index.js +++ b/src/matchers/data/index.js @@ -1,5 +1,7 @@ const { toHaveBodyEquals } = require('./toHaveBodyEquals'); +const { toHaveBodyMatchObject } = require('./toHaveBodyMatchObject'); module.exports = { toHaveBodyEquals, + toHaveBodyMatchObject, }; diff --git a/src/matchers/data/toHaveBodyEquals.js b/src/matchers/data/toHaveBodyEquals.js index 7a999d0..c0928b2 100644 --- a/src/matchers/data/toHaveBodyEquals.js +++ b/src/matchers/data/toHaveBodyEquals.js @@ -1,31 +1,6 @@ -const { getMatchingAdapter } = require('../../http-clients'); const { printDebugInfo } = require('../../utils/get-debug-info'); - -/** - * - * @param {HttpClientAdapter} adapter - */ -function getJSONBody(adapter) { - const body = adapter.getBody(); - - if (typeof body === 'string') { - try { - return JSON.parse(body); - } catch (e) { - return null; - } - } - - if (Buffer.isBuffer(body)) { - try { - return JSON.parse(body.toString()); - } catch (e) { - return null; - } - } - - return typeof body === 'object' ? body : null; -} +const { getJSONBody } = require('../../utils/json-body'); +const { getMatchingAdapter } = require('../../http-clients'); /** * @this {import('expect').MatcherUtils} @@ -38,7 +13,7 @@ function toHaveBodyEquals(actual, expectedValue) { // Headers are case-insensitive const contentTypeHeaderValue = Object.entries(headers).find(([name]) => name.toLowerCase() === 'content-type')?.[1]; - const isJson = contentTypeHeaderValue.toLowerCase().includes('application/json'); + const isJson = contentTypeHeaderValue?.toLowerCase().includes('application/json'); let body = adapter.getBody(); diff --git a/src/matchers/data/toHaveBodyMatchObject.js b/src/matchers/data/toHaveBodyMatchObject.js new file mode 100644 index 0000000..3e00c1f --- /dev/null +++ b/src/matchers/data/toHaveBodyMatchObject.js @@ -0,0 +1,88 @@ +const { getMatchingAdapter } = require('../../http-clients'); +const { printDebugInfo } = require('../../utils/get-debug-info'); +const { getJSONBody } = require('../../utils/json-body'); +const { jsonEquals } = require('../../utils/matchings/equals-json-body'); +const { jsonSubsetEquality, getJsonObjectSubset } = require('../../utils/matchings/equality-testers'); + +/** + * @this {import('expect').MatcherUtils} + */ +function toHaveBodyMatchObject(actual, expectedValue) { + const { matcherHint, printExpected, printDiffOrStringify, printReceived } = this.utils; + + if (typeof expectedValue !== 'object' || expectedValue === null) { + throw new Error('toHaveBodyMatchObject expects non-null object as the expected'); + } + + const adapter = getMatchingAdapter(actual); + const headers = adapter.getHeaders(); + + // Headers are case-insensitive + const contentTypeHeaderValue = Object.entries(headers).find(([name]) => name.toLowerCase() === 'content-type')?.[1]; + const isJson = contentTypeHeaderValue?.toLowerCase().includes('application/json'); + + let body = adapter.getBody(); + + let pass = false; + + if (isJson) { + body = getJSONBody(adapter); + + // This implementation taken from the `expect`, the code for the `toMatchObject` matcher + // https://github.com/jestjs/jest/blob/bd1c6db7c15c23788ca3e09c919138e48dd3b28a/packages/expect/src/matchers.ts#L895C1-L951C5 + + // Does not add iterator equality check as it JSON only support array and not custom iterable + // Custom implementation of equals and subset equality is added + // as we need to not allow non-json values in the expected object + // and undefined keys in the expected object mean that the key should not be present in the response body + pass = jsonEquals(body, expectedValue, [...this.customTesters, jsonSubsetEquality]); + } + + return { + pass, + message: () => { + // .not + if (pass) { + // If we pass the body must be json + + return [ + matcherHint('.not.toHaveBodyMatchObject', 'received', 'expected'), + '', + `Expected request to not have data:`, + printExpected(expectedValue), + ...(this.utils.stringify(printExpected) !== this.utils.stringify(body) + ? ['', `Received: ${printReceived(body)}`] + : []), + '', + printDebugInfo(adapter, { omitBody: true }), + ].join('\n'); + } + + if (!isJson) { + return [ + matcherHint('.toHaveBodyMatchObject', 'received', 'expected'), + '', + `Expected response to have json body`, + '', + printDebugInfo(adapter), + ].join('\n'); + } + + return [ + matcherHint('.toHaveBodyMatchObject', 'received', 'expected'), + '', + printDiffOrStringify( + expectedValue, + getJsonObjectSubset(body, expectedValue, this.customTesters), + 'Expected value', + 'Received value', + this.expand !== false, + ), + '', + printDebugInfo(adapter, { omitBody: true }), + ].join('\n'); + }, + }; +} + +module.exports = { toHaveBodyMatchObject }; diff --git a/src/utils/json-body.js b/src/utils/json-body.js new file mode 100644 index 0000000..4d7364b --- /dev/null +++ b/src/utils/json-body.js @@ -0,0 +1,27 @@ +/** + * + * @param {HttpClientAdapter} adapter + */ +function getJSONBody(adapter) { + const body = adapter.getBody(); + + if (typeof body === 'string') { + try { + return JSON.parse(body); + } catch { + return null; + } + } + + if (Buffer.isBuffer(body)) { + try { + return JSON.parse(body.toString()); + } catch { + return null; + } + } + + return typeof body === 'object' ? body : null; +} + +module.exports = { getJSONBody }; diff --git a/src/utils/matchings/equality-testers.js b/src/utils/matchings/equality-testers.js new file mode 100644 index 0000000..83c7105 --- /dev/null +++ b/src/utils/matchings/equality-testers.js @@ -0,0 +1,173 @@ +// Taken from `expect-utils` package +// From https://github.com/jestjs/jest/blob/bd1c6db7c15c23788ca3e09c919138e48dd3b28a/packages/expect-utils/src/utils.ts + +const { jsonEquals } = require('./equals-json-body'); + +function isObject(a) { + return a !== null && typeof a === 'object'; +} + +/** + * Retrieves an object's keys for evaluation by getObjectSubset. This evaluates + * the prototype chain for string keys but not for non-enumerable symbols. + * (Otherwise, it could find values such as a Set or Map's Symbol.toStringTag, + * with unexpected results.) + * + * + * Taken from @jest/expect-utils + * https://github.com/jestjs/jest/blob/bd1c6db7c15c23788ca3e09c919138e48dd3b28a/packages/expect-utils/src/utils.ts#L46-L57 + */ +function getObjectKeys(object) { + return [ + ...Object.keys(object), + ...Object.getOwnPropertySymbols(object).filter((s) => Object.getOwnPropertyDescriptor(object, s)?.enumerable), + ]; +} + +/** + * Checks if `hasOwnProperty(object, key)` up the prototype chain, stopping at `Object.prototype`. + * + * Taken from @jest/expect-utils + * https://github.com/jestjs/jest/blob/bd1c6db7c15c23788ca3e09c919138e48dd3b28a/packages/expect-utils/src/utils.ts#L28C1-L45C1 + */ +function hasPropertyInObject(object, key) { + const shouldTerminate = !object || typeof object !== 'object' || object === Object.prototype; + + if (shouldTerminate) { + return false; + } + + return Object.prototype.hasOwnProperty.call(object, key) || hasPropertyInObject(Object.getPrototypeOf(object), key); +} + +/** + * Taken from @jest/expect-utils + * https://github.com/jestjs/jest/blob/bd1c6db7c15c23788ca3e09c919138e48dd3b28a/packages/expect-utils/src/utils.ts#L112C1-L165C3 + * + * Strip properties from object that are not present in the subset. Useful for + * printing the diff for toMatchObject() without adding unrelated noise. + * + * @param isJson + * @param {import('expect').MatcherUtils} matcherUtils + * @param object + * @param subset + * @param customTesters + * @param seenReferences + */ +function getJsonObjectSubset(object, subset, customTesters = [], seenReferences = new WeakMap()) { + /* eslint-enable @typescript-eslint/explicit-module-boundary-types */ + if (Array.isArray(object)) { + if (Array.isArray(subset) && subset.length === object.length) { + // The map method returns correct subclass of subset. + return subset.map((sub, i) => getJsonObjectSubset(object[i], sub, customTesters)); + } + } else if (object instanceof Date) { + return object; + } else if (isObject(object) && isObject(subset)) { + if (jsonEquals(object, subset, [...customTesters, jsonSubsetEquality])) { + // Avoid unnecessary copy which might return Object instead of subclass. + return subset; + } + + const trimmed = {}; + seenReferences.set(object, trimmed); + + for (const key of getObjectKeys(object).filter((key) => hasPropertyInObject(subset, key))) { + trimmed[key] = seenReferences.has(object[key]) + ? seenReferences.get(object[key]) + : getJsonObjectSubset(object[key], subset[key], customTesters, seenReferences); + } + + if (getObjectKeys(trimmed).length > 0) { + return trimmed; + } + } + return object; +} + +function isObjectWithKeys(a) { + return ( + isObject(a) && + !(a instanceof Error) && + !Array.isArray(a) && + !(a instanceof Date) && + !(a instanceof Set) && + !(a instanceof Map) + ); +} + +/** + * Subset equality for valid JSON objects. + * @param {unknown} object + * @param {unknown} subset + * @param {import('expect').Tester[]} customTesters + * @returns {undefined|*} + */ +function jsonSubsetEquality(object, subset, customTesters = []) { + const filteredCustomTesters = customTesters.filter((t) => t !== jsonSubsetEquality); + + // subsetEquality needs to keep track of the references + // it has already visited to avoid infinite loops in case + // there are circular references in the subset passed to it. + + function subsetEqualityWithContext(seenReferencesObject = new WeakSet(), seenReferencesSubset = new WeakSet()) { + function tester(object, subset) { + if (!isObjectWithKeys(subset)) { + return undefined; + } + if (typeof object === 'object' && object !== null) { + if (seenReferencesObject.has(object)) { + return false; + } + seenReferencesObject.add(object); + } + if (typeof subset === 'object' && subset !== null) { + if (seenReferencesSubset.has(subset)) { + return false; + } + seenReferencesSubset.add(subset); + } + + const matchResult = getObjectKeys(subset).every((key) => { + let result; + + if (object == null) { + return false; + } + + // If subset[key] is undefined, than the object should not have the key + if (subset[key] === undefined) { + return !hasPropertyInObject(object, key); + } + + result = + hasPropertyInObject(object, key) && + jsonEquals(object[key], subset[key], [ + ...filteredCustomTesters, + subsetEqualityWithContext(seenReferencesObject, seenReferencesSubset), + ]); + + return result; + }); + + // The main goal of using seenReference is to avoid circular node on tree. + // It will only happen within a parent and its child, not a node and nodes next to it (same level) + // We should keep the reference for a parent and its child only + // Thus we should delete the reference immediately so that it doesn't interfere + // other nodes within the same level on tree. + if (typeof object === 'object' && object !== null) { + seenReferencesObject.delete(object); + } + if (typeof subset === 'object' && subset !== null) { + seenReferencesSubset.delete(subset); + } + return matchResult; + } + + return tester; + } + + return subsetEqualityWithContext()(object, subset); +} + +module.exports = { getJsonObjectSubset, jsonSubsetEquality }; diff --git a/src/utils/matchings/equals-json-body.js b/src/utils/matchings/equals-json-body.js new file mode 100644 index 0000000..330e0d8 --- /dev/null +++ b/src/utils/matchings/equals-json-body.js @@ -0,0 +1,241 @@ +// Taken from `@jest/expect-utils` package and modified +// Taken from https://github.com/jestjs/jest/blob/bd1c6db7c15c23788ca3e09c919138e48dd3b28a/packages/expect-utils/src/jasmineUtils.ts + +const jsonEquals = (a, b, customTesters, strictCheck) => { + customTesters = customTesters || []; + return eq(a, b, new WeakSet(), new WeakSet(), customTesters, strictCheck); +}; + +/** + * + * @param {any} obj + * @returns {false|value} + */ +function isAsymmetric(obj) { + return !!obj && isA('Function', obj.asymmetricMatch); +} + +/** + * + * @param a + * @param b + * @returns {undefined|boolean} + */ +function asymmetricMatch(a, b) { + const asymmetricA = isAsymmetric(a); + const asymmetricB = isAsymmetric(b); + + if (asymmetricA && asymmetricB) { + return undefined; + } + + if (asymmetricA) { + return a.asymmetricMatch(b); + } + + if (asymmetricB) { + return b.asymmetricMatch(a); + } +} + +/** + * + * @param {object} jsonBody + * @param {any} expected + * @param {WeakSet} visitedJsonBody + * @param {WeakSet} visitedExpected + * @param {import('expect').Tester[]} customTesters + * @param {boolean | undefined} strictCheck + * @returns {boolean} + */ +function eq(jsonBody, expected, visitedJsonBody, visitedExpected, customTesters, strictCheck) { + let result = true; + + // 2. Asymmetric matchers checks + const asymmetricResult = asymmetricMatch(jsonBody, expected); + if (asymmetricResult !== undefined) { + return asymmetricResult; + } + + // If none of the values are asymmetric matchers, we test for equality + if (jsonBody === expected) { + return true; + } + + // If one of them is not valid JSON value than they are not equal + if (!isValidJSONValue(jsonBody) || !isValidJSONValue(expected)) { + return false; + } + + // If one of them is not a valid shallow JSON object than they are not equal + if (!isValidShallowJsonObject(jsonBody) || !isValidShallowJsonObject(expected)) { + return false; + } + + if (jsonBody !== null) { + // If has circular reference, return false as it's not possible in JSON + if (visitedJsonBody.has(jsonBody)) { + return false; + } + + visitedJsonBody.add(jsonBody); + } + + if (expected !== null) { + // If has circular reference, return false as it's not possible in JSON + if (visitedExpected.has(expected)) { + return false; + } + + visitedExpected.add(expected); + } + + const isJsonBodyArray = Array.isArray(jsonBody); + const isExpectedArray = Array.isArray(expected); + + // If one of them is an array and the other is not, they are not equal + if (isJsonBodyArray !== isExpectedArray) { + return false; + } + + // If one of them is null (we already checked if both are equal) than they are not equal + if (jsonBody === null || expected === null) { + return false; + } + + /** + * + * @type {import('expect').TesterContext} + */ + const testerContext = { equals: jsonEquals }; + for (const item of customTesters) { + const customTesterResult = item.call(testerContext, jsonBody, expected, customTesters); + if (customTesterResult !== undefined) { + return customTesterResult; + } + } + + // Recursively compare objects and arrays. + // Compare array lengths to determine if jsonBody deep comparison is necessary. + if (strictCheck && isJsonBodyArray && jsonBody.length !== expected.length) { + return false; + } + + // Deep compare objects. + const jsonBodyKeys = keys(jsonBody, hasKey); + let key; + + const expectedKeys = keys(expected, hasKey); + // Add keys corresponding to asymmetric matchers if they miss in non strict check mode + if (!strictCheck) { + for (let index = 0; index !== expectedKeys.length; ++index) { + key = expectedKeys[index]; + if ((isAsymmetric(expected[key]) || expected[key] === undefined) && !hasKey(jsonBody, key)) { + jsonBodyKeys.push(key); + } + } + for (let index = 0; index !== jsonBodyKeys.length; ++index) { + key = jsonBodyKeys[index]; + if ((isAsymmetric(jsonBody[key]) || jsonBody[key] === undefined) && !hasKey(expected, key)) { + expectedKeys.push(key); + } + } + } + + // Ensure that both objects contain the same number of properties before comparing deep equality. + let size = jsonBodyKeys.length; + if (expectedKeys.length !== size) { + return false; + } + + while (size--) { + key = jsonBodyKeys[size]; + + // Deep compare each member + if (strictCheck) + result = + hasKey(expected, key) && + eq(jsonBody[key], expected[key], visitedJsonBody, visitedExpected, customTesters, strictCheck); + else + result = + (hasKey(expected, key) || isAsymmetric(jsonBody[key]) || jsonBody[key] === undefined) && + eq(jsonBody[key], expected[key], visitedJsonBody, visitedExpected, customTesters, strictCheck); + + if (!result) { + return false; + } + } + + return result; +} + +/** + * + * @param {object} obj + * @param {(obj: object, key: string) => boolean} hasKey + * @returns {*} + */ +function keys(obj, hasKey) { + const keys = []; + for (const key in obj) { + if (hasKey(obj, key)) { + keys.push(key); + } + } + return [ + ...keys, + ...Object.getOwnPropertySymbols(obj).filter((symbol) => Object.getOwnPropertyDescriptor(obj, symbol).enumerable), + ]; +} + +/** + * + * @param {any} obj + * @param {string | symbol} key + * @returns {boolean} + */ +function hasKey(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + +/** + * + * @param {string} typeName + * @param {unknown} value + * @returns {value is T} + * @template T + */ +function isA(typeName, value) { + return Object.prototype.toString.apply(value) === `[object ${typeName}]`; +} + +function isValidJSONValue(value) { + // valid JSON values: + // - string + // - number + // - object + // - array + // - boolean + // - null + + return ( + typeof value === 'string' || + typeof value === 'number' || + // Match object, `null` and arrays + typeof value === 'object' || + typeof value === 'boolean' + ); +} + +// Validate that the object itself is valid (without going into its keys) +// we assume that the object has typeof obj === 'object' +function isValidShallowJsonObject(obj) { + if (Array.isArray(obj) || obj === null) { + return true; + } + + const proto = Object.getPrototypeOf(obj); + return proto === Object.prototype || proto === null; +} + +module.exports = { jsonEquals }; diff --git a/test/matchers/data/toHaveBodyEqualsTo.test.js b/test/matchers/data/toHaveBodyEquals.test.js similarity index 100% rename from test/matchers/data/toHaveBodyEqualsTo.test.js rename to test/matchers/data/toHaveBodyEquals.test.js diff --git a/test/matchers/data/toHaveBodyEqualsTo.test.js.snapshot b/test/matchers/data/toHaveBodyEquals.test.js.snapshot similarity index 100% rename from test/matchers/data/toHaveBodyEqualsTo.test.js.snapshot rename to test/matchers/data/toHaveBodyEquals.test.js.snapshot diff --git a/test/matchers/data/toHaveBodyMatchObject.test.js b/test/matchers/data/toHaveBodyMatchObject.test.js new file mode 100644 index 0000000..3a8a6dc --- /dev/null +++ b/test/matchers/data/toHaveBodyMatchObject.test.js @@ -0,0 +1,1507 @@ +const { toHaveBodyMatchObject } = require('../../../src'); +const { describe, before, it } = require('node:test'); +const { buildServer } = require('../../helpers/server-helper.js'); +const { expect, JestAssertionError } = require('expect'); +const { getServerUrl } = require('../../helpers/server-helper'); +const { testClients } = require('../../helpers/supported-clients'); + +expect.extend({ toHaveBodyMatchObject }); + +describe('(.not).toHaveBodyMatchObject', () => { + let apiUrl = getServerUrl(); + + before(async () => { + await buildServer(); + }); + + for (const testClient of testClients) { + describe(`using ${testClient.name}`, () => { + describe('.toHaveBodyMatchObject', () => { + it('should fail when the received response body is plain/text data', async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'plain/text', + data: 'hello world', + }); + + try { + expect(response).toHaveBodyMatchObject({ + a: '1', + }); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject({ + a: '1', + }), + }), + ).toThrowError(JestAssertionError); + }); + + it('should fail when the received response body have content type plain/text even though the data itself is JSON string ', async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'plain/text', + data: JSON.stringify({}), + }); + + try { + expect(response).toHaveBodyMatchObject({}); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject({}), + }), + ).toThrowError(JestAssertionError); + }); + }); + + describe('.not.toHaveBodyMatchObject', () => { + it('should pass when the received response body is plain/text data', async () => { + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'plain/text', + data: 'hello world', + }); + + expect(response).not.toHaveBodyMatchObject({ + a: '1', + }); + + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject({ + a: '1', + }), + }); + }); + + it('should pass when the received response body have content type plain/text even though the data itself is JSON string ', async () => { + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'plain/text', + data: JSON.stringify({}), + }); + + expect(response).not.toHaveBodyMatchObject({}); + + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject({}), + }); + }); + }); + + // Taken from expect `toMatchObject` tests and modified + // https://github.com/jestjs/jest/blob/bd1c6db7c15c23788ca3e09c919138e48dd3b28a/packages/expect/src/__tests__/matchers.test.js#L2087-L2347 + describe('toMatchObject tests', () => { + class Foo { + get a() { + return undefined; + } + + get b() { + return 'b'; + } + } + + class Sub extends Foo { + get c() { + return 'c'; + } + } + + const withDefineProperty = (obj, key, val) => { + Object.defineProperty(obj, key, { + get() { + return val; + }, + }); + + return obj; + }; + + describe('circular references', () => { + it('should not pass when expected object contain circular references as its not possible in response object', async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const circularObjA1 = { a: 'hello' }; + circularObjA1.ref = circularObjA1; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { + a: 'hello', + ref: {}, + }, + }); + + try { + expect(response).toHaveBodyMatchObject(circularObjA1); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(circularObjA1), + }), + ).toThrowError(JestAssertionError); + }); + + it('should not pass when expected object contain transitive circular references as its not possible in response object', async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const transitiveCircularObjA1 = { a: 'hello' }; + transitiveCircularObjA1.nestedObj = { parentObj: transitiveCircularObjA1 }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { + a: 'hello', + nestedObj: { + parentObj: {}, + }, + }, + }); + + try { + expect(response).toHaveBodyMatchObject(transitiveCircularObjA1); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(transitiveCircularObjA1), + }), + ).toThrowError(JestAssertionError); + }); + }); + + describe('Matching', () => { + it(`response object does not have property and expected body has property undefined should match`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: {}, + }); + + expect(response).toHaveBodyMatchObject({ + a: undefined, + }); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject({ a: undefined }), + }); + + try { + expect(response).not.toHaveBodyMatchObject({ a: undefined }); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject({ a: undefined }), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: 'b', c: 'd'} and expected value is {a: 'b'}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: 'b' }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'b', c: 'd' }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: 'b', c: 'd'} and expected value is {a: 'b', c: 'd'}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: 'b', c: 'd' }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'b', c: 'd' }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: 'b', t: {x: {r: 'r'}, z: 'z'}} and expected value is {a: 'b', t: {z: 'z'}}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: 'b', t: { z: 'z' } }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'b', t: { x: { r: 'r' }, z: 'z' } }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: 'b', t: {x: {r: 'r'}, z: 'z'}} and expected value is {t: {x: {r: 'r'}}}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { t: { x: { r: 'r' } } }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'b', t: { x: { r: 'r' }, z: 'z' } }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: [3, 4, 5]}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: [3, 4, 5] }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: [3, 4, 5], b: 'b' }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: [3, 4, 5, 'v'], b: 'b'} and expected value is {a: [3, 4, 5, 'v']}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: [3, 4, 5, 'v'] }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: [3, 4, 5, 'v'], b: 'b' }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: 1, c: 2} and expected value is {a: expect.any(Number)}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: expect.any(Number) }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 1, c: 2 }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: {x: 'x', y: 'y'}} and expected value is {a: {x: expect.any(String)}}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: { x: expect.any(String) } }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: { x: 'x', y: 'y' } }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: null, b: 'b'} and expected value is {a: null}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: null }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: null, b: 'b' }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {b: 'b'} and expected value is {}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = {}; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { b: 'b' }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: [{a: 'a', b: 'b'}]} and expected value is {a: [{a: 'a'}]}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: [{ a: 'a' }] }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: [{ a: 'a', b: 'b' }] }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is [1, 2] and expected value is [1, 2]`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = [1, 2]; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: [1, 2], + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {} and expected value is {}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = {}; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: {}, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is [] and expected value is []`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = []; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: [], + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {message: 'bar'} and expected value is {message: 'bar'}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { message: 'bar' }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { message: 'bar' }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is {a: 'b'} and expected value is Object.assign(Object.create(null), {a: 'b'})`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = Object.assign(Object.create(null), { a: 'b' }); + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'b' }, + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + // Not testing asymmetric matcher throwing as it will fail to build the error because of the null prototype object + // expect(() => + // expect({response}).toEqual({ + // response: expect.not.toHaveBodyMatchObject(expectedValue), + // }), + // ).toThrowError(JestAssertionError); + }); + + it(`should pass when the response body is [0] and expected value is [-0] as -0 and 0 are the same value in JSON`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = [-0]; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: [0], + }); + + expect(response).toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).not.toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + }); + + describe('Not matching', () => { + it(`should not match when the response body is {a: 'b', c: 'd'} and expected value is {e: 'b'}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { e: 'b' }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'b', c: 'd' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: 'b', c: 'd'} and expected value is {a: 'b!', c: 'd'}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: 'b!', c: 'd' }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'b', c: 'd' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: 'a', c: 'd'} and expected value is {a: expect.any(Number)}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: expect.any(Number) }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'a', c: 'd' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: 'b', t: {x: {r: 'r'}, z: 'z'}} and expected value is {a: 'b', t: {z: [3]}}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: 'b', t: { z: [3] } }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'b', t: { x: { r: 'r' }, z: 'z' } }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: 'b', t: {x: {r: 'r'}, z: 'z'}} and expected value is {t: {l: {r: 'r'}}}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { t: { l: { r: 'r' } } }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 'b', t: { x: { r: 'r' }, z: 'z' } }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: [3, 4, 5, 6]}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: [3, 4, 5, 6] }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: [3, 4, 5], b: 'b' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: [3, 4]}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: [3, 4] }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: [3, 4, 5], b: 'b' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: [3, 4, 'v'], b: 'b'} and expected value is {a: ['v']}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: ['v'] }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: [3, 4, 'v'], b: 'b' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: {b: 4}}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: { b: 4 } }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: [3, 4, 5], b: 'b' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: {b: expect.any(String)}}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: { b: expect.any(String) } }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: [3, 4, 5], b: 'b' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is [1, 2] and expected value is [1, 3]`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = [1, 3]; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: [1, 2], + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: {}} and expected value is {a: new Set([])}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: new Set([]) }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: {} }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: new Date('2015-11-30').toISOString(), b: 'b'} and expected value is {a: new Date('2015-10-10')}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: new Date('2015-10-10') }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: new Date('2015-11-30').toISOString(), b: 'b' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: null, b: 'b'} and expected value is {a: '4'}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: '4' }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: null, b: 'b' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {} and expected value is {a: null}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: null }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: {}, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: [{a: 'a', b: 'b'}]} and expected value is {a: [{a: 'c'}]}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: [{ a: 'c' }] }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: [{ a: 'a', b: 'b' }] }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: 1, b: 1, c: 1, d: {e: {f: 555}}} and expected value is {d: {e: {f: 222}}}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { d: { e: { f: 222 } } }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: 1, b: 1, c: 1, d: { e: { f: 555 } } }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is [1, 2, 3] and expected value is [2, 3, 1]`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = [2, 3, 1]; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: [1, 2, 3], + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is [1, 2, 3] and expected value is [1, 2, 2]`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = [1, 2, 2]; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: [1, 2, 3], + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {c: 'd'} and expected value is Object.assign(Object.create(null), {a: 'b'})`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = Object.assign(Object.create(null), { a: 'b' }); + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { c: 'd' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + // Not testing asymmetric matcher throwing as it will fail to build the error because of the null prototype object + // expect(() => + // expect({response}).toEqual({ + // response: expect.toHaveBodyMatchObject(expectedValue), + // }), + // ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is { a: 'b', c: 'd', [Symbol.for('expect-http-client-matchers').toString()]: 'expect-http-client-matchers' } and expected value is {a: 'c', [Symbol.for('expect-http-client-matchers')]: expect.any(String)}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { a: 'c', [Symbol.for('expect-http-client-matchers')]: expect.any(String) }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { + a: 'b', + c: 'd', + [Symbol.for('expect-http-client-matchers').toString()]: 'expect-http-client-matchers', + }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is { a: 'b', c: 'd', [Symbol.for('expect-http-client-matchers').toString()]: 'expect-http-client-matchers' } and expected value is {a: 'b', [Symbol.for('expect-http-client-matchers')]: 'expect-http-client-matchers'}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { + a: 'b', + [Symbol.for('expect-http-client-matchers')]: 'expect-http-client-matchers', + }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { + a: 'b', + c: 'd', + [Symbol.for('expect-http-client-matchers').toString()]: 'expect-http-client-matchers', + }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: 'b', c: 'd', [Symbol.for('expect-http-client-matchers').toString()]: 'expect-http-client-matchers'} and expected value is {a: 'b', c: 'd', [Symbol.for('expect-http-client-matchers')]: 'expect-http-client-matchers'}`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = { + a: 'b', + c: 'd', + [Symbol.for('expect-http-client-matchers')]: 'expect-http-client-matchers', + }; + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { + a: 'b', + c: 'd', + [Symbol.for('expect-http-client-matchers').toString()]: 'expect-http-client-matchers', + }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: undefined, b: 'b'} and expected value is new Foo() as cant match because classes are not valid JSON value`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = new Foo(); + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: undefined, b: 'b' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {a: undefined, b: 'b', c: 'c'} and expected value is new Sub()`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = new Sub(); + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { a: undefined, b: 'b', c: 'c' }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + + it(`should not match when the response body is {d: 4} and expected value is withDefineProperty(new Sub(), 'd', 4)`, async (t) => { + // Should have the assert snapshot assertion + t.plan(1); + + const expectedValue = withDefineProperty(new Sub(), 'd', 4); + + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: { d: 4 }, + }); + + expect(response).not.toHaveBodyMatchObject(expectedValue); + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expectedValue), + }); + + try { + expect(response).toHaveBodyMatchObject(expectedValue); + } catch (e) { + t.assert.snapshot(e); + } + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expectedValue), + }), + ).toThrowError(JestAssertionError); + }); + }); + + describe('Validation', () => { + for (const expected of [null, 4, 'some string', true, undefined]) { + it(`throws when expected is ${JSON.stringify(expected)}`, async () => { + const response = await testClient.post(`${apiUrl}/body`, { + contentType: 'application/json', + data: {}, + }); + + expect(() => expect(response).toHaveBodyMatchObject(expected)).toThrowError( + 'toHaveBodyMatchObject expects non-null object as the expected', + ); + + expect(() => expect(response).not.toHaveBodyMatchObject(expected)).toThrowError( + 'toHaveBodyMatchObject expects non-null object as the expected', + ); + + expect(() => + expect({ response }).toEqual({ + response: expect.toHaveBodyMatchObject(expected), + }), + ).toThrowError('toHaveBodyMatchObject expects non-null object as the expected'); + + expect(() => + expect({ response }).toEqual({ + response: expect.not.toHaveBodyMatchObject(expected), + }), + ).toThrowError('toHaveBodyMatchObject expects non-null object as the expected'); + }); + } + }); + }); + }); + } +}); diff --git a/test/matchers/data/toHaveBodyMatchObject.test.js.snapshot b/test/matchers/data/toHaveBodyMatchObject.test.js.snapshot new file mode 100644 index 0000000..85e5c4c --- /dev/null +++ b/test/matchers/data/toHaveBodyMatchObject.test.js.snapshot @@ -0,0 +1,1141 @@ +exports[`(.not).toHaveBodyMatchObject > using axios > .toHaveBodyMatchObject > should fail when the received response body have content type plain/text even though the data itself is JSON string 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +Expected response to have json body + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "plain/text", + "content-length": "2", + "connection": "keep-alive" + }, + "body": {} +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > .toHaveBodyMatchObject > should fail when the received response body is plain/text data 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +Expected response to have json body + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "plain/text", + "content-length": "11", + "connection": "keep-alive" + }, + "body": "hello world" +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > response object does not have property and expected body has property undefined should match 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": undefined} + +Received: {} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "2", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is [0] and expected value is [-0] as -0 and 0 are the same value in JSON 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +[-0] + +Received: [0] + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "3", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is [1, 2] and expected value is [1, 2] 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +[1, 2] + +Received: [1, 2] + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "5", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is [] and expected value is [] 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +[] + +Received: [] + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "2", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: 'b', c: 'd'} and expected value is {a: 'b', c: 'd'} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": "b", "c": "d"} + +Received: {"a": "b", "c": "d"} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "17", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: 'b', c: 'd'} and expected value is {a: 'b'} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": "b"} + +Received: {"a": "b", "c": "d"} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "17", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: 'b', t: {x: {r: 'r'}, z: 'z'}} and expected value is {a: 'b', t: {z: 'z'}} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": "b", "t": {"z": "z"}} + +Received: {"a": "b", "t": {"x": {"r": "r"}, "z": "z"}} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "37", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: 'b', t: {x: {r: 'r'}, z: 'z'}} and expected value is {t: {x: {r: 'r'}}} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"t": {"x": {"r": "r"}}} + +Received: {"a": "b", "t": {"x": {"r": "r"}, "z": "z"}} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "37", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: 'b'} and expected value is Object.assign(Object.create(null), {a: 'b'}) 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": "b"} + +Received: {"a": "b"} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "9", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: 1, c: 2} and expected value is {a: expect.any(Number)} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": Any} + +Received: {"a": 1, "c": 2} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "13", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: [3, 4, 5, 'v'], b: 'b'} and expected value is {a: [3, 4, 5, 'v']} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": [3, 4, 5, "v"]} + +Received: {"a": [3, 4, 5, "v"], "b": "b"} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "25", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: [3, 4, 5]} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": [3, 4, 5]} + +Received: {"a": [3, 4, 5], "b": "b"} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "21", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: [{a: 'a', b: 'b'}]} and expected value is {a: [{a: 'a'}]} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": [{"a": "a"}]} + +Received: {"a": [{"a": "a", "b": "b"}]} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "25", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: null, b: 'b'} and expected value is {a: null} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": null} + +Received: {"a": null, "b": "b"} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "18", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {a: {x: 'x', y: 'y'}} and expected value is {a: {x: expect.any(String)}} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"a": {"x": Any}} + +Received: {"a": {"x": "x", "y": "y"}} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "23", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {b: 'b'} and expected value is {} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{} + +Received: {"b": "b"} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "9", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {message: 'bar'} and expected value is {message: 'bar'} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{"message": "bar"} + +Received: {"message": "bar"} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "17", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Matching > should pass when the response body is {} and expected value is {} 1`] = ` +expect(received).not.toHaveBodyMatchObject(expected) + +Expected request to not have data: +{} + +Received: {} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "2", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is [1, 2, 3] and expected value is [1, 2, 2] 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Array [ + 1, + 2, +- 2, ++ 3, + ] + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "7", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is [1, 2, 3] and expected value is [2, 3, 1] 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Array [ ++ 1, + 2, + 3, +- 1, + ] + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "7", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is [1, 2] and expected value is [1, 3] 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Array [ + 1, +- 3, ++ 2, + ] + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "5", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is { a: 'b', c: 'd', [Symbol.for('expect-http-client-matchers').toString()]: 'expect-http-client-matchers' } and expected value is {a: 'b', [Symbol.for('expect-http-client-matchers')]: 'expect-http-client-matchers'} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 0 + + Object { + "a": "b", +- Symbol(expect-http-client-matchers): "expect-http-client-matchers", + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "85", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is { a: 'b', c: 'd', [Symbol.for('expect-http-client-matchers').toString()]: 'expect-http-client-matchers' } and expected value is {a: 'c', [Symbol.for('expect-http-client-matchers')]: expect.any(String)} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 2 ++ Received value + 1 + + Object { +- "a": "c", +- Symbol(expect-http-client-matchers): Any, ++ "a": "b", + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "85", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: 'a', c: 'd'} and expected value is {a: expect.any(Number)} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { +- "a": Any, ++ "a": "a", + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "17", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: 'b', c: 'd', [Symbol.for('expect-http-client-matchers').toString()]: 'expect-http-client-matchers'} and expected value is {a: 'b', c: 'd', [Symbol.for('expect-http-client-matchers')]: 'expect-http-client-matchers'} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 0 + + Object { + "a": "b", + "c": "d", +- Symbol(expect-http-client-matchers): "expect-http-client-matchers", + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "85", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: 'b', c: 'd'} and expected value is {a: 'b!', c: 'd'} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { +- "a": "b!", ++ "a": "b", + "c": "d", + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "17", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: 'b', c: 'd'} and expected value is {e: 'b'} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 2 + + Object { +- "e": "b", ++ "a": "b", ++ "c": "d", + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "17", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: 'b', t: {x: {r: 'r'}, z: 'z'}} and expected value is {a: 'b', t: {z: [3]}} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 3 ++ Received value + 1 + + Object { + "a": "b", + "t": Object { +- "z": Array [ +- 3, +- ], ++ "z": "z", + }, + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "37", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: 'b', t: {x: {r: 'r'}, z: 'z'}} and expected value is {t: {l: {r: 'r'}}} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 2 + + Object { + "t": Object { +- "l": Object { ++ "x": Object { + "r": "r", + }, ++ "z": "z", + }, + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "37", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: 1, b: 1, c: 1, d: {e: {f: 555}}} and expected value is {d: {e: {f: 222}}} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { + "d": Object { + "e": Object { +- "f": 222, ++ "f": 555, + }, + }, + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "39", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: [3, 4, 'v'], b: 'b'} and expected value is {a: ['v']} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 0 ++ Received value + 2 + + Object { + "a": Array [ ++ 3, ++ 4, + "v", + ], + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "23", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: [3, 4, 5, 6]} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 0 + + Object { + "a": Array [ + 3, + 4, + 5, +- 6, + ], + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "21", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: [3, 4]} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 0 ++ Received value + 1 + + Object { + "a": Array [ + 3, + 4, ++ 5, + ], + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "21", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: {b: 4}} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 3 ++ Received value + 5 + + Object { +- "a": Object { +- "b": 4, +- }, ++ "a": Array [ ++ 3, ++ 4, ++ 5, ++ ], + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "21", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: [3, 4, 5], b: 'b'} and expected value is {a: {b: expect.any(String)}} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 3 ++ Received value + 5 + + Object { +- "a": Object { +- "b": Any, +- }, ++ "a": Array [ ++ 3, ++ 4, ++ 5, ++ ], + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "21", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: [{a: 'a', b: 'b'}]} and expected value is {a: [{a: 'c'}]} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { + "a": Array [ + Object { +- "a": "c", ++ "a": "a", + }, + ], + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "25", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: new Date('2015-11-30').toISOString(), b: 'b'} and expected value is {a: new Date('2015-10-10')} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { +- "a": 2015-10-10T00:00:00.000Z, ++ "a": "2015-11-30T00:00:00.000Z", + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "40", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: null, b: 'b'} and expected value is {a: '4'} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { +- "a": "4", ++ "a": null, + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "18", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: undefined, b: 'b', c: 'c'} and expected value is new Sub() 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 4 + +- Sub {} ++ Object { ++ "b": "b", ++ "c": "c", ++ } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "17", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: undefined, b: 'b'} and expected value is new Foo() as cant match because classes are not valid JSON value 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 3 + +- Foo {} ++ Object { ++ "b": "b", ++ } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "9", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {a: {}} and expected value is {a: new Set([])} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { +- "a": Set {}, ++ "a": Object {}, + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "8", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {c: 'd'} and expected value is Object.assign(Object.create(null), {a: 'b'}) 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { +- "a": "b", ++ "c": "d", + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "9", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {d: 4} and expected value is withDefineProperty(new Sub(), 'd', 4) 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 3 + +- Sub {} ++ Object { ++ "d": 4, ++ } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "7", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > Not matching > should not match when the response body is {} and expected value is {a: null} 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 3 ++ Received value + 1 + +- Object { +- "a": null, +- } ++ Object {} + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "2", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > circular references > should not pass when expected object contain circular references as its not possible in response object 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { + "a": "hello", +- "ref": [Circular], ++ "ref": Object {}, + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "22", + "connection": "keep-alive" + } +} +`; + +exports[`(.not).toHaveBodyMatchObject > using axios > toMatchObject tests > circular references > should not pass when expected object contain transitive circular references as its not possible in response object 1`] = ` +expect(received).toHaveBodyMatchObject(expected) + +- Expected value - 1 ++ Received value + 1 + + Object { + "a": "hello", + "nestedObj": Object { +- "parentObj": [Circular], ++ "parentObj": Object {}, + }, + } + +------------ +response is: +{ + "url": "http://127.0.0.1:54607/body", + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "42", + "connection": "keep-alive" + } +} +`; diff --git a/test/utils/matchings/equality-testers.test.js b/test/utils/matchings/equality-testers.test.js new file mode 100644 index 0000000..8a1950d --- /dev/null +++ b/test/utils/matchings/equality-testers.test.js @@ -0,0 +1,297 @@ +// Taken from @jest/expect-utils and modified +// https://github.com/jestjs/jest/blob/bd1c6db7c15c23788ca3e09c919138e48dd3b28a/packages/expect-utils/src/__tests__/utils.test.ts + +const { describe, test } = require('node:test'); +const { expect } = require('expect'); +const { jsonSubsetEquality, getJsonObjectSubset } = require('../../../src/utils/matchings/equality-testers'); +const util = require('node:util'); + +describe('Modified jest code', () => { + describe('getJsonObjectSubset', () => { + for (const [object, subset, expected] of [ + [{ a: 'b', c: 'd' }, { a: 'd' }, { a: 'b' }], + [{ a: [1, 2], b: 'b' }, { a: [3, 4] }, { a: [1, 2] }], + [[{ a: 'b', c: 'd' }], [{ a: 'z' }], [{ a: 'b' }]], + [ + [1, 2], + [1, 2, 3], + [1, 2], + ], + [{ a: [1] }, { a: [1, 2] }, { a: [1] }], + [new Date('2015-11-30'), new Date('2016-12-30'), new Date('2015-11-30')], + ]) { + test( + `expect(getJsonObjectSubset(${util.inspect(object)}, ${util.inspect(subset)}))` + + `.toEqual(${util.inspect(expected)})`, + () => { + expect(getJsonObjectSubset(object, subset)).toEqual(expected); + }, + ); + } + + describe('returns the object instance if the subset has no extra properties', () => { + test('Date', () => { + const object = new Date('2015-11-30'); + const subset = new Date('2016-12-30'); + + expect(getJsonObjectSubset(object, subset)).toBe(object); + }); + }); + + describe('returns the subset instance if its property values are equal', () => { + test('Object', () => { + const object = { key0: 'zero', key1: 'one', key2: 'two' }; + const subset = { key0: 'zero', key2: 'two' }; + + expect(getJsonObjectSubset(object, subset)).toBe(subset); + }); + }); + + describe('calculating subsets of objects with circular references', () => { + test('simple circular references', () => { + // type CircularObj = {a?: string; b?: string; ref?: unknown}; + + const nonCircularObj = { a: 'world', b: 'something' }; + + const circularObjA = { a: 'hello' }; + circularObjA.ref = circularObjA; + + const circularObjB = { a: 'world' }; + circularObjB.ref = circularObjB; + + const primitiveInsteadOfRef = { b: 'something' }; + primitiveInsteadOfRef.ref = 'not a ref'; + + const nonCircularRef = { b: 'something' }; + nonCircularRef.ref = {}; + + expect(getJsonObjectSubset(circularObjA, nonCircularObj)).toEqual({ + a: 'hello', + }); + expect(getJsonObjectSubset(nonCircularObj, circularObjA)).toEqual({ + a: 'world', + }); + + expect(getJsonObjectSubset(circularObjB, circularObjA)).toEqual(circularObjB); + + expect(getJsonObjectSubset(primitiveInsteadOfRef, circularObjA)).toEqual({ + ref: 'not a ref', + }); + expect(getJsonObjectSubset(nonCircularRef, circularObjA)).toEqual({ + ref: {}, + }); + }); + + test('transitive circular references', () => { + // type CircularObj = {a?: string; nestedObj?: unknown}; + + const nonCircularObj = { a: 'world', b: 'something' }; + + const transitiveCircularObjA = { a: 'hello' }; + transitiveCircularObjA.nestedObj = { parentObj: transitiveCircularObjA }; + + const transitiveCircularObjB = { a: 'world' }; + transitiveCircularObjB.nestedObj = { parentObj: transitiveCircularObjB }; + + const primitiveInsteadOfRef = {}; + primitiveInsteadOfRef.nestedObj = { otherProp: 'not the parent ref' }; + + const nonCircularRef = {}; + nonCircularRef.nestedObj = { otherProp: {} }; + + expect(getJsonObjectSubset(transitiveCircularObjA, nonCircularObj)).toEqual({ + a: 'hello', + }); + expect(getJsonObjectSubset(nonCircularObj, transitiveCircularObjA)).toEqual({ + a: 'world', + }); + + expect(getJsonObjectSubset(transitiveCircularObjB, transitiveCircularObjA)).toEqual(transitiveCircularObjB); + + expect(getJsonObjectSubset(primitiveInsteadOfRef, transitiveCircularObjA)).toEqual({ + nestedObj: { otherProp: 'not the parent ref' }, + }); + expect(getJsonObjectSubset(nonCircularRef, transitiveCircularObjA)).toEqual({ + nestedObj: { otherProp: {} }, + }); + }); + }); + }); + + describe('jsonSubsetEquality()', () => { + test('matching object returns true', () => { + expect(jsonSubsetEquality({ foo: 'bar' }, { foo: 'bar' })).toBe(true); + }); + + test('object without keys is undefined', () => { + expect(jsonSubsetEquality('foo', 'bar')).toBeUndefined(); + }); + + test('objects to not match', () => { + expect(jsonSubsetEquality({ foo: 'bar' }, { foo: 'baz' })).toBe(false); + expect(jsonSubsetEquality('foo', { foo: 'baz' })).toBe(false); + }); + + test('null does not return errors', () => { + expect(jsonSubsetEquality(null, { foo: 'bar' })).not.toBeTruthy(); + }); + + test('undefined does not return errors', () => { + expect(jsonSubsetEquality(undefined, { foo: 'bar' })).not.toBeTruthy(); + }); + + describe('not matching any circular references as they are not supported in JSON', () => { + test('simple circular references', () => { + // type CircularObj = {a?: string; ref?: unknown}; + + const circularObjA1 = { a: 'hello' }; + circularObjA1.ref = circularObjA1; + + const circularObjA2 = { a: 'hello' }; + circularObjA2.ref = circularObjA2; + + const circularObjB = { a: 'world' }; + circularObjB.ref = circularObjB; + + const primitiveInsteadOfRef = {}; + primitiveInsteadOfRef.ref = 'not a ref'; + + // This is true as we did not check for circular references as the subset is empty + expect(jsonSubsetEquality(circularObjA1, {})).toBe(true); + + expect(jsonSubsetEquality({}, circularObjA1)).toBe(false); + expect(jsonSubsetEquality(circularObjA2, circularObjA1)).toBe(false); + expect(jsonSubsetEquality(circularObjB, circularObjA1)).toBe(false); + expect(jsonSubsetEquality(primitiveInsteadOfRef, circularObjA1)).toBe(false); + }); + + test('referenced object on same level should not regarded as circular reference', () => { + const referencedObj = { abc: 'def' }; + const object = { + a: { abc: 'def' }, + b: { abc: 'def', zzz: 'zzz' }, + }; + const thisIsNotCircular = { + a: referencedObj, + b: referencedObj, + }; + expect(jsonSubsetEquality(object, thisIsNotCircular)).toBeTruthy(); + }); + + test('referenced object on object and subset should not be regarded as circular references', () => { + const referencedObj = { abc: 'def' }; + const object = { + a: referencedObj, + b: referencedObj, + }; + const thisIsNotCircular = { + a: referencedObj, + b: referencedObj, + }; + expect(jsonSubsetEquality(object, thisIsNotCircular)).toBeTruthy(); + }); + + test('transitive circular references should always return false as they are not possible in JSON', () => { + // type CircularObj = {a: string; nestedObj?: unknown}; + + const transitiveCircularObjA1 = { a: 'hello' }; + transitiveCircularObjA1.nestedObj = { parentObj: transitiveCircularObjA1 }; + + const transitiveCircularObjA2 = { a: 'hello' }; + transitiveCircularObjA2.nestedObj = { + parentObj: transitiveCircularObjA2, + }; + + const transitiveCircularObjB = { a: 'world' }; + transitiveCircularObjB.nestedObj = { + parentObj: transitiveCircularObjB, + }; + + const primitiveInsteadOfRef = { + parentObj: 'not the parent ref', + }; + + // This is true as we did not check for circular references as the subset is empty + expect(jsonSubsetEquality(transitiveCircularObjA1, {})).toBe(true); + expect(jsonSubsetEquality({}, transitiveCircularObjA1)).toBe(false); + expect(jsonSubsetEquality(transitiveCircularObjA2, transitiveCircularObjA1)).toBe(false); + expect(jsonSubsetEquality(transitiveCircularObjB, transitiveCircularObjA1)).toBe(false); + expect(jsonSubsetEquality(primitiveInsteadOfRef, transitiveCircularObjA1)).toBe(false); + }); + }); + + describe('matching subsets with symbols', () => { + describe('same symbol', () => { + test('objects to not match with value diff', () => { + const symbol = Symbol('foo'); + expect(jsonSubsetEquality({ [symbol]: 1 }, { [symbol]: 2 })).toBe(false); + }); + + test('objects to match with non-enumerable symbols', () => { + const symbol = Symbol('foo'); + const foo = {}; + Object.defineProperty(foo, symbol, { + enumerable: false, + value: 1, + }); + const bar = {}; + Object.defineProperty(bar, symbol, { + enumerable: false, + value: 2, + }); + expect(jsonSubsetEquality(foo, bar)).toBe(true); + }); + }); + + describe('different symbol', () => { + test('objects to not match with same value', () => { + expect(jsonSubsetEquality({ [Symbol('foo')]: 1 }, { [Symbol('foo')]: 2 })).toBe(false); + }); + test('objects to match with non-enumerable symbols', () => { + const foo = {}; + Object.defineProperty(foo, Symbol('foo'), { + enumerable: false, + value: 1, + }); + const bar = {}; + Object.defineProperty(bar, Symbol('foo'), { + enumerable: false, + value: 2, + }); + expect(jsonSubsetEquality(foo, bar)).toBe(true); + }); + }); + }); + + describe('subset is not object with keys', () => { + test('returns true if subset has keys', () => { + expect(jsonSubsetEquality({ foo: 'bar' }, { foo: 'bar' })).toBe(true); + }); + test('returns true if subset has Symbols', () => { + const symbol = Symbol('foo'); + expect(jsonSubsetEquality({ [symbol]: 'bar' }, { [symbol]: 'bar' })).toBe(true); + }); + test('returns undefined if subset has no keys', () => { + expect(jsonSubsetEquality('foo', 'bar')).toBeUndefined(); + }); + test('returns undefined if subset is null', () => { + expect(jsonSubsetEquality({ foo: 'bar' }, null)).toBeUndefined(); + }); + test('returns undefined if subset is Error', () => { + expect(jsonSubsetEquality({ foo: 'bar' }, new Error())).toBeUndefined(); + }); + test('returns undefined if subset is Array', () => { + expect(jsonSubsetEquality({ foo: 'bar' }, [])).toBeUndefined(); + }); + test('returns undefined if subset is Date', () => { + expect(jsonSubsetEquality({ foo: 'bar' }, new Date())).toBeUndefined(); + }); + test('returns undefined if subset is Set', () => { + expect(jsonSubsetEquality({ foo: 'bar' }, new Set())).toBeUndefined(); + }); + test('returns undefined if subset is Map', () => { + expect(jsonSubsetEquality({ foo: 'bar' }, new Map())).toBeUndefined(); + }); + }); + }); +}); diff --git a/types-test/expect-type-tests/types.test.ts b/types-test/expect-type-tests/types.test.ts index 387ce20..bf9fa76 100644 --- a/types-test/expect-type-tests/types.test.ts +++ b/types-test/expect-type-tests/types.test.ts @@ -369,6 +369,16 @@ async function run() { expect(res).not.toHaveBodyEquals(expect.anything()); expect(res).toEqual(expect.toHaveBodyEquals(expect.anything())); expect(res).toEqual(expect.not.toHaveBodyEquals(expect.anything())); + + expect(res).toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).not.toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).toEqual(expect.toHaveBodyMatchObject({ foo: 'bar' })); + expect(res).toEqual(expect.not.toHaveBodyMatchObject({ foo: 'bar' })); + + expect(res).toHaveBodyMatchObject(expect.anything()); + expect(res).not.toHaveBodyMatchObject(expect.anything()); + expect(res).toEqual(expect.toHaveBodyMatchObject(expect.anything())); + expect(res).toEqual(expect.not.toHaveBodyMatchObject(expect.anything())); } run(); diff --git a/types-test/jest-all-type-tests/types.test.ts b/types-test/jest-all-type-tests/types.test.ts index b4ea629..e81d8ae 100644 --- a/types-test/jest-all-type-tests/types.test.ts +++ b/types-test/jest-all-type-tests/types.test.ts @@ -368,6 +368,16 @@ async function run() { expect(res).not.toHaveBodyEquals(expect.anything()); expect(res).toEqual(expect.toHaveBodyEquals(expect.anything())); expect(res).toEqual(expect.not.toHaveBodyEquals(expect.anything())); + + expect(res).toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).not.toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).toEqual(expect.toHaveBodyMatchObject({ foo: 'bar' })); + expect(res).toEqual(expect.not.toHaveBodyMatchObject({ foo: 'bar' })); + + expect(res).toHaveBodyMatchObject(expect.anything()); + expect(res).not.toHaveBodyMatchObject(expect.anything()); + expect(res).toEqual(expect.toHaveBodyMatchObject(expect.anything())); + expect(res).toEqual(expect.not.toHaveBodyMatchObject(expect.anything())); } run(); diff --git a/types-test/jest-partial-type-tests/types.test.ts b/types-test/jest-partial-type-tests/types.test.ts index b4ea629..e81d8ae 100644 --- a/types-test/jest-partial-type-tests/types.test.ts +++ b/types-test/jest-partial-type-tests/types.test.ts @@ -368,6 +368,16 @@ async function run() { expect(res).not.toHaveBodyEquals(expect.anything()); expect(res).toEqual(expect.toHaveBodyEquals(expect.anything())); expect(res).toEqual(expect.not.toHaveBodyEquals(expect.anything())); + + expect(res).toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).not.toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).toEqual(expect.toHaveBodyMatchObject({ foo: 'bar' })); + expect(res).toEqual(expect.not.toHaveBodyMatchObject({ foo: 'bar' })); + + expect(res).toHaveBodyMatchObject(expect.anything()); + expect(res).not.toHaveBodyMatchObject(expect.anything()); + expect(res).toEqual(expect.toHaveBodyMatchObject(expect.anything())); + expect(res).toEqual(expect.not.toHaveBodyMatchObject(expect.anything())); } run(); diff --git a/types-test/vitest-all-type-tests/types.test.ts b/types-test/vitest-all-type-tests/types.test.ts index b4ea629..e81d8ae 100644 --- a/types-test/vitest-all-type-tests/types.test.ts +++ b/types-test/vitest-all-type-tests/types.test.ts @@ -368,6 +368,16 @@ async function run() { expect(res).not.toHaveBodyEquals(expect.anything()); expect(res).toEqual(expect.toHaveBodyEquals(expect.anything())); expect(res).toEqual(expect.not.toHaveBodyEquals(expect.anything())); + + expect(res).toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).not.toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).toEqual(expect.toHaveBodyMatchObject({ foo: 'bar' })); + expect(res).toEqual(expect.not.toHaveBodyMatchObject({ foo: 'bar' })); + + expect(res).toHaveBodyMatchObject(expect.anything()); + expect(res).not.toHaveBodyMatchObject(expect.anything()); + expect(res).toEqual(expect.toHaveBodyMatchObject(expect.anything())); + expect(res).toEqual(expect.not.toHaveBodyMatchObject(expect.anything())); } run(); diff --git a/types-test/vitest-partial-type-tests/types.test.ts b/types-test/vitest-partial-type-tests/types.test.ts index b4ea629..e81d8ae 100644 --- a/types-test/vitest-partial-type-tests/types.test.ts +++ b/types-test/vitest-partial-type-tests/types.test.ts @@ -368,6 +368,16 @@ async function run() { expect(res).not.toHaveBodyEquals(expect.anything()); expect(res).toEqual(expect.toHaveBodyEquals(expect.anything())); expect(res).toEqual(expect.not.toHaveBodyEquals(expect.anything())); + + expect(res).toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).not.toHaveBodyMatchObject({ foo: 'bar' }); + expect(res).toEqual(expect.toHaveBodyMatchObject({ foo: 'bar' })); + expect(res).toEqual(expect.not.toHaveBodyMatchObject({ foo: 'bar' })); + + expect(res).toHaveBodyMatchObject(expect.anything()); + expect(res).not.toHaveBodyMatchObject(expect.anything()); + expect(res).toEqual(expect.toHaveBodyMatchObject(expect.anything())); + expect(res).toEqual(expect.not.toHaveBodyMatchObject(expect.anything())); } run(); diff --git a/types/shared.d.ts b/types/shared.d.ts index e4bee4f..a3c6f81 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -317,6 +317,18 @@ export interface CustomMatchers extends Record { * @param {unknown} expectedBody the expected body to match with **/ toHaveBodyEquals(expectedBody: unknown): R; + + /** + * Use .toHaveBodyMatchObject when checking if the response body match the expected body. + * + * The implementation of the matcher is similar to the implementation of [expect's toMatchObject](https://jestjs.io/docs/en/expect#tomatchobjectobject) + * except only valid JSON values or asymmetric matchers are supported in the expected body + * + * Undefined values in the expected body means that the response body should not contain the key at all (not even with null value) + * + * @param {unknown} expectedBody the expected body to match with + **/ + toHaveBodyMatchObject(expectedBody: unknown): R; } // noinspection JSUnusedGlobalSymbols @@ -639,4 +651,16 @@ export interface SharedMatchers { * @param {unknown} expectedBody the expected body to match with **/ toHaveBodyEquals(expectedBody: unknown): R; + + /** + * Use .toHaveBodyMatchObject when checking if the response body match the expected body. + * + * The implementation of the matcher is similar to the implementation of [expect's toMatchObject](https://jestjs.io/docs/en/expect#tomatchobjectobject) + * except only valid JSON values or asymmetric matchers are supported in the expected body + * + * Undefined values in the expected body means that the response body should not contain the key at all (not even with null value) + * + * @param {unknown} expectedBody the expected body to match with + **/ + toHaveBodyMatchObject(expectedBody: unknown): R; }