diff --git a/.gitignore b/.gitignore index 8d46df6..54d57f9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ dist .yarnrc.yml.temp # rollup -.rollup.cache \ No newline at end of file +.rollup.cache + +# test +coverage diff --git a/README.md b/README.md index a49f7e1..9dabfa5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # shared +[![Test Coverage](https://img.shields.io/badge/coverage-38.11%25-yellow.svg)](https://github.com/ummgoban/shared) + ummgoban 공통 패키지입니다. - ummgoban 도메인에서 공용으로 사용하는 type, utils, http client, hook 등을 관리합니다. @@ -14,12 +16,9 @@ yarn add @ummgoban/shared # exports - `@ummgoban/shared` +- `@ummgoban/shared/lib` +- `@ummgoban/shared/network` - `@ummgoban/shared/react` -- `@ummgoban/shared/types` -- `@ummgoban/shared/utils` -- `@ummgoban/shared/http` -- `@ummgoban/shared/http/error` -- `@ummgoban/shared/http/api-client` # dev @@ -33,9 +32,24 @@ NPM_TOKEN=ghp_... ## test ```bash -yarn test +yarn test # 테스트 실행 +yarn test:watch # 테스트 실시간 감시 모드 +yarn test:coverage # 테스트 커버리지 보고서 생성 ``` +## 테스트 커버리지 + +현재 프로젝트의 테스트 커버리지는 38.11%입니다. 커버리지 보고서는 `coverage` 디렉토리에서 확인할 수 있습니다. + +```bash +yarn test:coverage # 커버리지 보고서 생성 후 coverage/index.html 확인 +``` + +커버리지 보고서에서 제외된 파일: +- `**/index.ts` 파일들 +- 테스트 파일들 (`*.spec.ts`, `*.test.ts`) +- 타입 정의 파일들 (`*.d.ts`) + ## dev ```bash diff --git a/package.json b/package.json index 5526886..a1f2407 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "ci": "./.script/ci.sh", "build": "rm -rf dist && rollup -c", "test": "vitest run", + "test:watch": "vitest watch", + "test:coverage": "vitest run --coverage", "dev": "vite", "type-check": "tsc --noEmit", "lint": "eslint src --ext .ts,.tsx --fix", @@ -66,6 +68,7 @@ "@typescript-eslint/eslint-plugin": "^8.23.0", "@typescript-eslint/parser": "^8.23.0", "@vitejs/plugin-react": "^4.5.0", + "@vitest/coverage-v8": "3.1.4", "axios": "^1.7.4", "dotenv": "^16.4.7", "eslint": "^9.27.0", diff --git a/src/network/error/http-error.spec.ts b/src/network/error/http-error.spec.ts new file mode 100644 index 0000000..99ba820 --- /dev/null +++ b/src/network/error/http-error.spec.ts @@ -0,0 +1,16 @@ +import {CustomError} from './http-error'; + +describe('CustomError', () => { + it('should create an instance with error message ', () => { + const error = new CustomError(new Error('test error message')); + expect(error).toBeDefined(); + expect(error.message).toBe('test error message'); + expect(error.errorMessage).toBe('test error message'); + }); + it('should create an instance with default error message ', () => { + const error = new CustomError(new Error()); + expect(error).toBeDefined(); + expect(error.message).toBe('서버에서 오류가 발생했어요. 문제가 지속된다면 문의주세요'); + expect(error.errorMessage).toBe('서버에서 오류가 발생했어요. 문제가 지속된다면 문의주세요'); + }); +}); diff --git a/src/network/error/http-error.type.ts b/src/network/error/http-error.ts similarity index 84% rename from src/network/error/http-error.type.ts rename to src/network/error/http-error.ts index 9d24a9e..1cd4084 100644 --- a/src/network/error/http-error.type.ts +++ b/src/network/error/http-error.ts @@ -5,8 +5,9 @@ export class CustomError extends Error { errorMessage?: string; constructor(args: CustomErrorType) { - if (args instanceof Error) { + if (args instanceof Error && args.message) { super(args.message); + this.errorMessage = args.message; } else { super('서버에서 오류가 발생했어요. 문제가 지속된다면 문의주세요'); this.errorMessage = '서버에서 오류가 발생했어요. 문제가 지속된다면 문의주세요'; diff --git a/src/network/error/index.ts b/src/network/error/index.ts index 24bd35c..6f9a98b 100644 --- a/src/network/error/index.ts +++ b/src/network/error/index.ts @@ -1,2 +1,2 @@ // http error -export * from './http-error.type'; +export * from './http-error'; diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts new file mode 100644 index 0000000..c0949d5 --- /dev/null +++ b/src/react/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-pull-down-refresh'; diff --git a/src/react/hooks/use-pull-down-refresh/index.ts b/src/react/hooks/use-pull-down-refresh/index.ts new file mode 100644 index 0000000..7e9146c --- /dev/null +++ b/src/react/hooks/use-pull-down-refresh/index.ts @@ -0,0 +1,3 @@ +import usePullDownRefresh from './use-pull-down-refresh'; + +export {usePullDownRefresh}; diff --git a/src/react/hooks/use-pull-down-refresh/use-pull-down-refresh.spec.tsx b/src/react/hooks/use-pull-down-refresh/use-pull-down-refresh.spec.tsx new file mode 100644 index 0000000..8f75c75 --- /dev/null +++ b/src/react/hooks/use-pull-down-refresh/use-pull-down-refresh.spec.tsx @@ -0,0 +1,82 @@ +import usePullDownRefresh from './use-pull-down-refresh'; + +import {act, renderHook} from '@testing-library/react'; + +describe('usePullDownRefresh', () => { + let callbackPromise: ReturnType; + + beforeEach(() => { + callbackPromise = vi.fn().mockResolvedValue(undefined); + }); + + it('should call callback when onRefresh is called', async () => { + const {result} = renderHook(() => usePullDownRefresh(callbackPromise)); + + await act(async () => { + await result.current.onRefresh(); + }); + + expect(callbackPromise).toHaveBeenCalledTimes(1); + }); + + it('should set refreshing to true while refreshing and false when done', async () => { + // 콜백 함수가 해결될 때까지 기다리는 프로미스 생성 + let resolveCallback: () => void; + const waitForCallback = new Promise(resolve => { + resolveCallback = resolve; + }); + + callbackPromise = vi.fn().mockImplementation(() => waitForCallback); + + const {result} = renderHook(() => usePullDownRefresh(callbackPromise)); + + // 초기 상태 확인 + expect(result.current.refreshing).toBe(false); + + // onRefresh 호출 시작 + let onRefreshPromise: Promise; + act(() => { + onRefreshPromise = result.current.onRefresh(); + }); + + // refreshing 상태가 true로 변경되었는지 확인 + expect(result.current.refreshing).toBe(true); + + // 콜백 함수 완료 + act(() => { + resolveCallback(); + }); + + // onRefresh가 완료될 때까지 기다림 + await act(async () => { + await onRefreshPromise; + }); + + // refreshing 상태가 false로 돌아왔는지 확인 + expect(result.current.refreshing).toBe(false); + }); + + it('should handle errors and set refreshing to false', async () => { + // 에러를 발생시키는 콜백 함수 + const error = new Error('Test error'); + callbackPromise = vi.fn().mockRejectedValue(error); + + // 콘솔 에러 모니터링 + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const {result} = renderHook(() => usePullDownRefresh(callbackPromise)); + + await act(async () => { + await result.current.onRefresh(); + }); + + // 에러가 콘솔에 기록되었는지 확인 + expect(consoleSpy).toHaveBeenCalledWith(error); + + // 에러가 발생해도 refreshing 상태가 false로 돌아왔는지 확인 + expect(result.current.refreshing).toBe(false); + + // 스파이 정리 + consoleSpy.mockRestore(); + }); +}); diff --git a/src/react/hooks/use-pull-down-refresh/use-pull-down-refresh.tsx b/src/react/hooks/use-pull-down-refresh/use-pull-down-refresh.tsx new file mode 100644 index 0000000..9990ee1 --- /dev/null +++ b/src/react/hooks/use-pull-down-refresh/use-pull-down-refresh.tsx @@ -0,0 +1,21 @@ +import {useCallback, useState} from 'react'; + +const usePullDownRefresh = (callback: () => Promise) => { + const [refreshing, setRefreshing] = useState(false); + + const onRefresh = useCallback(async () => { + setRefreshing(true); + + try { + await callback(); + } catch (reason) { + console.error(reason); + } finally { + setRefreshing(false); + } + }, [callback]); + + return {refreshing, onRefresh}; +}; + +export default usePullDownRefresh; diff --git a/src/react/index.ts b/src/react/index.ts index 03be03e..11fbfc1 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -1 +1,2 @@ +export * from './hooks'; export * from './provider'; diff --git a/src/react/provider/combine-provider.spec.tsx b/src/react/provider/combine-provider.spec.tsx index d1d9fd6..e66f84e 100644 --- a/src/react/provider/combine-provider.spec.tsx +++ b/src/react/provider/combine-provider.spec.tsx @@ -1,6 +1,6 @@ import {createContext, useContext} from 'react'; + import {render, screen} from '@testing-library/react'; -import {describe, it, expect} from 'vitest'; import {combineProviders, ProviderEntry, Providers} from './combine-provider'; diff --git a/vitest.config.ts b/vitest.config.ts index d01011a..9cbe676 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,6 +7,17 @@ export default defineConfig({ setupFiles: './vitest.setup.ts', coverage: { reporter: ['text', 'json', 'html'], + exclude: [ + '**/index.ts', + 'node_modules/**', + '.yarn/**', + 'dist/**', + '**/*.d.ts', + '**/*.type.ts', + '**/*.config.*', + '**/*.{test,spec}.{js,jsx,ts,tsx}', + '__tests__/**', + ], }, }, }); diff --git a/yarn.lock b/yarn.lock index b9b8f7a..402232b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,7 +12,7 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0": +"@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" dependencies: @@ -163,7 +163,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.3, @babel/parser@npm:^7.27.4": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.3, @babel/parser@npm:^7.27.4": version: 7.27.4 resolution: "@babel/parser@npm:7.27.4" dependencies: @@ -229,7 +229,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3": version: 7.27.3 resolution: "@babel/types@npm:7.27.3" dependencies: @@ -239,6 +239,13 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^1.0.2": + version: 1.0.2 + resolution: "@bcoe/v8-coverage@npm:1.0.2" + checksum: f4e6f55817645fc1b543aa0bbd6ffceb7b9ff3052e8c92c493a0a71831e6b8ae97d73e123b048cb98ef9d9e31afae018a60795f2e27a6f3e94a1ec7abedac85d + languageName: node + linkType: hard + "@csstools/color-helpers@npm:^5.0.2": version: 5.0.2 resolution: "@csstools/color-helpers@npm:5.0.2" @@ -642,6 +649,13 @@ __metadata: languageName: node linkType: hard +"@istanbuljs/schema@npm:^0.1.2": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 5282759d961d61350f33d9118d16bcaed914ebf8061a52f4fa474b2cb08720c9c81d165e13b82f2e5a8a212cc5af482f0c6fc1ac27b9e067e5394c9a6ed186c9 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.8 resolution: "@jridgewell/gen-mapping@npm:0.3.8" @@ -674,7 +688,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: @@ -1355,6 +1369,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^8.23.0 "@typescript-eslint/parser": ^8.23.0 "@vitejs/plugin-react": ^4.5.0 + "@vitest/coverage-v8": 3.1.4 axios: ^1.7.4 dotenv: ^16.4.7 eslint: ^9.27.0 @@ -1517,6 +1532,32 @@ __metadata: languageName: node linkType: hard +"@vitest/coverage-v8@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/coverage-v8@npm:3.1.4" + dependencies: + "@ampproject/remapping": ^2.3.0 + "@bcoe/v8-coverage": ^1.0.2 + debug: ^4.4.0 + istanbul-lib-coverage: ^3.2.2 + istanbul-lib-report: ^3.0.1 + istanbul-lib-source-maps: ^5.0.6 + istanbul-reports: ^3.1.7 + magic-string: ^0.30.17 + magicast: ^0.3.5 + std-env: ^3.9.0 + test-exclude: ^7.0.1 + tinyrainbow: ^2.0.0 + peerDependencies: + "@vitest/browser": 3.1.4 + vitest: 3.1.4 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 745c8e7e7898ff54b97bdcc917606d488aa195701d4be3eeae0b4e5c1d4ff16af723e2837f050f7bdbd69fd3d37e131962f19f837e34dd7e0f0fa6dd8fae9c8d + languageName: node + linkType: hard + "@vitest/expect@npm:3.1.4": version: 3.1.4 resolution: "@vitest/expect@npm:3.1.4" @@ -2465,7 +2506,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.0": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.0": version: 4.4.1 resolution: "debug@npm:4.4.1" dependencies: @@ -3581,7 +3622,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2": +"glob@npm:^10.2.2, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -3756,6 +3797,13 @@ __metadata: languageName: node linkType: hard +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: d2df2da3ad40ca9ee3a39c5cc6475ef67c8f83c234475f24d8e9ce0dc80a2c82df8e1d6fa78ddd1e9022a586ea1bd247a615e80a5cd9273d90111ddda7d9e974 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.2.0 resolution: "http-cache-semantics@npm:4.2.0" @@ -4194,6 +4242,45 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 2367407a8d13982d8f7a859a35e7f8dd5d8f75aae4bb5484ede3a9ea1b426dc245aff28b976a2af48ee759fdd9be374ce2bd2669b644f31e76c5f46a2e29a831 + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: ^3.0.0 + make-dir: ^4.0.0 + supports-color: ^7.1.0 + checksum: fd17a1b879e7faf9bb1dc8f80b2a16e9f5b7b8498fe6ed580a618c34df0bfe53d2abd35bf8a0a00e628fb7405462576427c7df20bbe4148d19c14b431c974b21 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": ^0.3.23 + debug: ^4.1.1 + istanbul-lib-coverage: ^3.0.0 + checksum: 8dd6f2c1e2ecaacabeef8dc9ab52c4ed0a6036310002cf7f46ea6f3a5fb041da8076f5350e6a6be4c60cd4f231c51c73e042044afaf44820d857d92ecfb8ab6c + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.7": + version: 3.1.7 + resolution: "istanbul-reports@npm:3.1.7" + dependencies: + html-escaper: ^2.0.0 + istanbul-lib-report: ^3.0.0 + checksum: 2072db6e07bfbb4d0eb30e2700250636182398c1af811aea5032acb219d2080f7586923c09fa194029efd6b92361afb3dcbe1ebcc3ee6651d13340f7c6c4ed95 + languageName: node + linkType: hard + "jackspeak@npm:^3.1.2": version: 3.4.3 resolution: "jackspeak@npm:3.4.3" @@ -4517,6 +4604,26 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.3.5": + version: 0.3.5 + resolution: "magicast@npm:0.3.5" + dependencies: + "@babel/parser": ^7.25.4 + "@babel/types": ^7.25.4 + source-map-js: ^1.2.0 + checksum: 668f07ade907a44bccfc9a9321588473f6d5fa25329aa26b9ad9a3bf87cc2e6f9c482cbdd3e33c0b9ab9b79c065630c599cc055a12f881c8c924ee0d7282cdce + languageName: node + linkType: hard + +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: ^7.5.3 + checksum: bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + "make-fetch-happen@npm:^14.0.3": version: 14.0.3 resolution: "make-fetch-happen@npm:14.0.3" @@ -5675,7 +5782,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.6.0, semver@npm:^7.7.1": +"semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.7.1": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: @@ -5827,7 +5934,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 4eb0cd997cdf228bc253bcaff9340afeb706176e64868ecd20efbe6efea931465f43955612346d6b7318789e5265bdc419bc7669c1cebe3db0eb255f57efa76b @@ -6145,6 +6252,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^7.0.1": + version: 7.0.1 + resolution: "test-exclude@npm:7.0.1" + dependencies: + "@istanbuljs/schema": ^0.1.2 + glob: ^10.4.1 + minimatch: ^9.0.4 + checksum: e5a49a054bf2da74467dd8149b202166e36275c0dc2c9585f7d34de99c6d055d2287ac8d2a8e4c27c59b893acbc671af3fa869e8069a58ad117250e9c01c726b + languageName: node + linkType: hard + "text-extensions@npm:^1.0.0": version: 1.9.0 resolution: "text-extensions@npm:1.9.0"