diff --git a/.husky/pre-commit b/.husky/pre-commit index d2ae35e..000d962 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,3 +2,5 @@ . "$(dirname "$0")/_/husky.sh" yarn lint-staged + +yarn test diff --git a/package.json b/package.json index 3799672..5ec14e4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "lint:fix": "next lint --fix", "prepare": "husky install", "export": "next export", - "format": "prettier --write ." + "format": "prettier --write .", + "test": "jest" }, "dependencies": { "@chakra-ui/icons": "2.1.1", @@ -34,6 +35,7 @@ }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "3.4.2", + "@jest/globals": "^29.7.0", "@types/node": "18.0.0", "@types/react": "18.2.47", "@types/react-copy-to-clipboard": "^5.0.7", @@ -49,12 +51,24 @@ "eslint-plugin-react": "7.28.0", "eslint-plugin-react-hooks": "4.3.0", "husky": "7.0.4", + "jest": "^29.7.0", "lint-staged": "12.3.2", "prettier": "2.7.1", + "ts-jest": "^29.2.5", "typescript": "4.7.4" }, "lint-staged": { "*.{js,ts,tsx}": "eslint --fix", "*.{js,css,md,ts,tsx}": "prettier --write" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "transform": { + "node_modules/variables/.+\\.(j|t)sx?$": "ts-jest" + }, + "transformIgnorePatterns": [ + "node_modules/(?!variables/.*)" + ] } } diff --git a/src/lib/catchy.test.ts b/src/lib/catchy.test.ts new file mode 100644 index 0000000..8709aca --- /dev/null +++ b/src/lib/catchy.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, jest, test } from '@jest/globals'; + +import { catchy } from './catchy'; + +async function getUser() { + throw new Error('user not found'); +} + +async function getMetadata() { + return Promise.resolve('metadata found!'); +} + +describe('Catchy Module - Single Function', () => { + test('Given a promise that throws an error, when calling catchy, then it should return the error', async () => { + const [err, user] = await catchy(getUser()); + expect(err).toBeInstanceOf(Error); + expect(user).toBeUndefined(); + }); + + test('Given a promise that resolves, when calling catchy, then it should return the value', async () => { + const [err, metadata] = await catchy(getMetadata()); + expect(err).toBeUndefined(); + expect(metadata).toBe('metadata found!'); + }); + + test('Given a promise that throws an error, when calling catchy with an error to catch, then it should return the error', async () => { + const [err, user] = await catchy(getUser(), [Error]); + expect(err).toBeInstanceOf(Error); + expect(user).toBeUndefined(); + }); +}); + +describe('Catchy Module - Multiple Functions', () => { + test('Given a multiple function calls, when some of them throw an error at the end, then it should return the error', async () => { + const [errMetadata, metadata] = await catchy(getMetadata()); + expect(errMetadata).toBeUndefined(); + expect(metadata).toBe('metadata found!'); + + const [errUser, user] = await catchy(getUser()); + expect(errUser).toBeInstanceOf(Error); + expect(user).toBeUndefined(); + }); + + test('Given a multiple function calls, when some of them throw an error at the beginning, then it should return the error', async () => { + const [errUser, user] = await catchy(getUser()); + expect(errUser).toBeInstanceOf(Error); + expect(user).toBeUndefined(); + + const [errMetadata, metadata] = await catchy(getMetadata()); + expect(errMetadata).toBeUndefined(); + expect(metadata).toBe('metadata found!'); + }); +}); + +describe('Catchy Module - Verbose Mode', () => { + test('Given a promise that throws an error, when calling catchy with verbose mode, then it should return the error and log it', async () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + const [err, user] = await catchy(getUser(), undefined, true); + + expect(err).toBeInstanceOf(Error); + expect(user).toBeUndefined(); + expect(consoleError).toHaveBeenCalled(); + + consoleError.mockRestore(); + }); + + test('Given a promise that resolves, when calling catchy with verbose mode, then it should return the value', async () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const [err, metadata] = await catchy(getMetadata(), undefined, true); + expect(err).toBeUndefined(); + expect(metadata).toBe('metadata found!'); + expect(consoleError).not.toHaveBeenCalled(); + + consoleError.mockRestore(); + }); +}); diff --git a/src/lib/catchy.ts b/src/lib/catchy.ts new file mode 100644 index 0000000..8681b2e --- /dev/null +++ b/src/lib/catchy.ts @@ -0,0 +1,25 @@ +// this utility function wraps a promise and catches errors of a specific type. +// it makes your code more catch-strict and less error - prone +// since the `try-catch` must parse the errors to be caught +async function catchy Error>( + promise: Promise, + errsToCatch?: Array, + verbose?: boolean, +): Promise<[undefined, T] | [InstanceType]> { + try { + const v = await promise; + return [undefined, v] as [undefined, T]; + } catch (err) { + /* eslint-disable curly */ + /* eslint-disable no-console */ + if (verbose) console.error(err); + + if (!errsToCatch || errsToCatch.some((errType) => err instanceof errType)) { + return [err as InstanceType]; + } + + throw err; + } +} + +export { catchy };