diff --git a/README.md b/README.md index 66d876c9..c84a86ca 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ If you've come here to help contribute - Thanks! Take a look at the [contributin - [.toBeEven()](#tobeeven) - [.toBeOdd()](#tobeodd) - [.toBeWithin(start, end)](#tobewithinstart-end) + - [.toBeWithinPercent(mid, percent)](#tobewithinpercentmid-percent) - [Object](#object) - [.toBeObject()](#tobeobject) - [.toContainKey(key)](#tocontainkeykey) @@ -599,6 +600,18 @@ test('passes when number is within given bounds', () => { }); ``` +#### .toBeWithinPercent(mid, percent) + +Use `.toBeWithinPercent` when checking if a number is within x percent of a target number. + +```js +test('passes when number is within x percent of target', () => { + expect(55).toBeWithinPercent(50, 10); + expect(20).toBeWithinPercent(10, 100); + expect(100).not.toBeWithinPercent(10, 5); +}); +``` + ### Object #### .toBeObject() @@ -640,6 +653,25 @@ test('passes when object contains all keys', () => { }); ``` +#### .toContainKeysWithinPercent([keyObjects]) + +Use `.toContainKeysWithinPercent` when checking if an object has all of the provided keys and that the value of these keys is within x percent of a target value. + +```js +test('passes when object contains all keys', () => { + const data1 = { a: 55, b: 1 }; + const data2 = { a: 45, b: 1 } + const data3 = { a: 20, b: 2 } + const data4 = { a: 50 } + const keys = [{key: "a", target: 50, percent: 10}, {key: "b", target: 1, percent: 0}] + + expect(data1).toContainKeysWithinPercent(keys); + expect(data2).toContainKeysWithinPercent(keys); + expect(data3).not.toContainKeysWithinPercent(keys); + expect(data4).not.toContainKeysWithinPercent(keys); +}); +``` + #### .toContainAllKeys([keys]) Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. diff --git a/src/matchers/toBeWithinPercent/__snapshots__/index.test.js.snap b/src/matchers/toBeWithinPercent/__snapshots__/index.test.js.snap new file mode 100644 index 00000000..dd51e61d --- /dev/null +++ b/src/matchers/toBeWithinPercent/__snapshots__/index.test.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`.not.toBeWithinPercent fails when given number is within x percent of the target number 1`] = ` +"expect(received).not.toBeWithinPercent(expected) + +Expected number to not be within percent of target: + target: 20 percent: 100% +Received: + 55" +`; + +exports[`.not.toBeWithinPercent fails when given number is within x percent of the target number 1`] = ` +"expect(received).not.toBeWithinPercent(expected) + +Expected number to not be within percent of target: + target: 20 percent: 100% +Received: + 25" +`; + +exports[`.toBeWithinPercent fails when given number is not within x percent of the target number 1`] = ` +"expect(received).toBeWithinPercent(expected) + +Expected number to be within percent of target: + target: 20 percent: 5% +Received: + 100" +`; + +exports[`.toBeWithinPercent fails when given number is not within x percent of the target number 1`] = ` +"expect(received).toBeWithinPercent(expected) + +Expected number to be within percent of target: + target: 50 percent: 10% +Received: + 56" +`; diff --git a/src/matchers/toBeWithinPercent/index.js b/src/matchers/toBeWithinPercent/index.js new file mode 100644 index 00000000..99f6eda9 --- /dev/null +++ b/src/matchers/toBeWithinPercent/index.js @@ -0,0 +1,30 @@ +import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; + +import predicate from './predicate'; + +const passMessage = (number, target, percent) => () => + matcherHint('.not.toBeWithinPercent') + + '\n\n' + + 'Expected number to not be within percent of target:\n' + + ` target: ${printExpected(target)} percent: ${printExpected(percent)}%\n` + + 'Received:\n' + + ` ${printReceived(number)}`; + +const failMessage = (number, target, percent) => () => + matcherHint('.toBeWithinPercent') + + '\n\n' + + 'Expected number to be within percent of target:\n' + + ` target: ${printExpected(target)} percent: ${printExpected(percent)}%\n` + + 'Received:\n' + + ` ${printReceived(number)}`; + +export default { + toBeWithinPercent: (number, target, percent) => { + const pass = predicate(number, target, percent); + if (pass) { + return { pass: true, message: passMessage(number, target, percent) }; + } + + return { pass: false, message: failMessage(number, target, percent) }; + } +}; diff --git a/src/matchers/toBeWithinPercent/index.test.js b/src/matchers/toBeWithinPercent/index.test.js new file mode 100644 index 00000000..064adcfe --- /dev/null +++ b/src/matchers/toBeWithinPercent/index.test.js @@ -0,0 +1,23 @@ +import matcher from './'; + +expect.extend(matcher); + +describe('.toBeWithinPercent', () => { + test('passes when given number is within x percent of the target number', () => { + expect(55).toBeWithinPercent(50, 10); + }); + + test('fails when given number is not within x percent of the target number', () => { + expect(() => expect(56).toBeWithinPercent(50, 10)).toThrowErrorMatchingSnapshot(); + }); +}); + +describe('.not.toBeWithinPercent', () => { + test('passes when given number is not within x percent of the target number', () => { + expect(100).not.toBeWithinPercent(20, 5); + }); + + test('fails when given number is within x percent of the target number', () => { + expect(() => expect(25).not.toBeWithinPercent(20, 100)).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/src/matchers/toBeWithinPercent/predicate.js b/src/matchers/toBeWithinPercent/predicate.js new file mode 100644 index 00000000..d84753ba --- /dev/null +++ b/src/matchers/toBeWithinPercent/predicate.js @@ -0,0 +1,2 @@ +export default (number, target, percent) => + number <= target * (1 + percent / 100) && number >= target * (1 - percent / 100); diff --git a/src/matchers/toBeWithinPercent/predicate.test.js b/src/matchers/toBeWithinPercent/predicate.test.js new file mode 100644 index 00000000..3dcd4311 --- /dev/null +++ b/src/matchers/toBeWithinPercent/predicate.test.js @@ -0,0 +1,11 @@ +import predicate from './predicate'; + +describe('toBeWithinPercent Predicate', () => { + test('returns true when given number is within percent of target', () => { + expect(predicate(55, 50, 10)).toBe(true); + }); + + test('returns false when given number is not within percent of target', () => { + expect(predicate(60, 50, 10)).toBe(false); + }); +}); diff --git a/src/matchers/toContainKeysWithinPercent/__snapshots__/index.test.js.snap b/src/matchers/toContainKeysWithinPercent/__snapshots__/index.test.js.snap new file mode 100644 index 00000000..7b8b6b1e --- /dev/null +++ b/src/matchers/toContainKeysWithinPercent/__snapshots__/index.test.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`.not.toContainKeys fails when object contains all keys and they are within x percent of target 1`] = ` +"expect(received).not.toContainKeysWithinPercent(expected) + +Expected object to not contain all keys or the keys' values to not be within x percent of target: + [{\\"key\\": \\"a\\", \\"percent\\": 10, \\"target\\": 50}, {\\"key\\": \\"b\\", \\"percent\\": 0, \\"target\\": 5}, {\\"key\\": \\"c\\", \\"percent\\": 100, \\"target\\": 1}] +Received: + {\\"a\\": 55, \\"b\\": 5, \\"c\\": 1}" +`; + +exports[`.toContainKeys fails when object does not contain all keys or they are not within x percent of target 1`] = ` +"expect(received).toContainKeysWithinPercent(expected) + +Expected object to contain all keys and the keys' values to be within x percent of target: + [{\\"key\\": \\"a\\", \\"percent\\": 10, \\"target\\": 50}, {\\"key\\": \\"b\\", \\"percent\\": 0, \\"target\\": 5}, {\\"key\\": \\"c\\", \\"percent\\": 100, \\"target\\": 1}] +Received: + {\\"a\\": 56, \\"b\\": 5, \\"c\\": 1}" +`; diff --git a/src/matchers/toContainKeysWithinPercent/index.js b/src/matchers/toContainKeysWithinPercent/index.js new file mode 100644 index 00000000..089845bc --- /dev/null +++ b/src/matchers/toContainKeysWithinPercent/index.js @@ -0,0 +1,30 @@ +import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; + +import predicate from './predicate'; + +const passMessage = (valueObj, keysArray) => () => + matcherHint('.not.toContainKeysWithinPercent') + + '\n\n' + + "Expected object to not contain all keys or the keys' values to not be within x percent of target:\n" + + ` ${printExpected(keysArray)}\n` + + 'Received:\n' + + ` ${printReceived(valueObj)}`; + +const failMessage = (valueObj, keysArray) => () => + matcherHint('.toContainKeysWithinPercent') + + '\n\n' + + "Expected object to contain all keys and the keys' values to be within x percent of target:\n" + + ` ${printExpected(keysArray)}\n` + + 'Received:\n' + + ` ${printReceived(valueObj)}`; + +export default { + toContainKeysWithinPercent: (valueObj, keysArray) => { + const pass = predicate(valueObj, keysArray); + if (pass) { + return { pass: true, message: passMessage(valueObj, keysArray) }; + } + + return { pass: false, message: failMessage(valueObj, keysArray) }; + } +}; diff --git a/src/matchers/toContainKeysWithinPercent/index.test.js b/src/matchers/toContainKeysWithinPercent/index.test.js new file mode 100644 index 00000000..8023414f --- /dev/null +++ b/src/matchers/toContainKeysWithinPercent/index.test.js @@ -0,0 +1,43 @@ +import matcher from './'; + +expect.extend(matcher); + +const data1 = { a: 55, b: 5, c: 1 }; +const data2 = { a: 56, b: 5, c: 1 }; + +const keys = [ + { + key: 'a', + target: 50, + percent: 10 + }, + { + key: 'b', + target: 5, + percent: 0 + }, + { + key: 'c', + target: 1, + percent: 100 + } +]; +describe('.toContainKeys', () => { + test('passes when object contains all keys and they are within x percent of target', () => { + expect(data1).toContainKeysWithinPercent(keys); + }); + + test('fails when object does not contain all keys or they are not within x percent of target', () => { + expect(() => expect(data2).toContainKeysWithinPercent(keys)).toThrowErrorMatchingSnapshot(); + }); +}); + +describe('.not.toContainKeys', () => { + test('passes when object does not contain all keys or they are not within x percent of target', () => { + expect(data2).not.toContainKeysWithinPercent(keys); + }); + + test('fails when object contains all keys and they are within x percent of target', () => { + expect(() => expect(data1).not.toContainKeysWithinPercent(keys)).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/src/matchers/toContainKeysWithinPercent/predicate.js b/src/matchers/toContainKeysWithinPercent/predicate.js new file mode 100644 index 00000000..e95f7af6 --- /dev/null +++ b/src/matchers/toContainKeysWithinPercent/predicate.js @@ -0,0 +1,8 @@ +export default (valueObj, keys) => + keys.every(keyObj => { + return ( + valueObj[keyObj.key] && + valueObj[keyObj.key] <= keyObj.target * (1 + keyObj.percent / 100) && + valueObj[keyObj.key] >= keyObj.target * (1 - keyObj.percent / 100) + ); + }); diff --git a/src/matchers/toContainKeysWithinPercent/predicate.test.js b/src/matchers/toContainKeysWithinPercent/predicate.test.js new file mode 100644 index 00000000..9e43a697 --- /dev/null +++ b/src/matchers/toContainKeysWithinPercent/predicate.test.js @@ -0,0 +1,32 @@ +import predicate from './predicate'; + +const data1 = { a: 55, b: 5, c: 1 }; +const data2 = { a: 56, b: 5, c: 1 }; + +const keys = [ + { + key: 'a', + target: 50, + percent: 10 + }, + { + key: 'b', + target: 5, + percent: 0 + }, + { + key: 'c', + target: 1, + percent: 100 + } +]; + +describe('.toContainKeysWithinPercent', () => { + test('passes when object contains all keys and they are within x percent of target', () => { + expect(predicate(data1, keys)).toBe(true); + }); + + test('fails when object does not contain all keys or they are not within x percent of target', () => { + expect(predicate(data2, keys)).toBe(false); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index de45f8c2..c4668019 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -186,6 +186,14 @@ declare namespace jest { */ toBeWithin(start: number, end: number): R; + /** + * Use `.toBeWithinPercent` when checking if a number is within x percent of a target number. + * + * @param {Number} target + * @param {Number} percent + */ + toBeWithinPercent(target: number, percent: number): R; + /** * Use `.toBeObject` when checking if a value is an `Object`. */ @@ -561,6 +569,13 @@ declare namespace jest { */ toContainKeys(keys: string[]): any; + /** + * Use `.toContainKeysWithinPercent` when checking if an object has all of the provided keys and the keys' values are within x percent of a target value. + * + * @param {Array.} keyObjects + */ + toContainKeysWithinPercent(keyObjects: object[]): any; + /** * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. *