From 89a7cc22857dd017e02d4b1a13983e8a58f3f907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=92?= =?UTF-8?q?=E1=85=AE=E1=86=AB?= Date: Wed, 20 Aug 2025 20:46:13 +0900 Subject: [PATCH 1/7] Init Commit From 5169567c479e32d18a626a13c348841579e7acd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=92?= =?UTF-8?q?=E1=85=AE=E1=86=AB?= Date: Wed, 20 Aug 2025 20:58:08 +0900 Subject: [PATCH 2/7] =?UTF-8?q?Chore:=20package.json=20=EC=84=B8=ED=8C=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 ++ pnpm-lock.yaml | 34 +++++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index b01b2b4b..68a7441b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react-dom": "19.1.0" }, "devDependencies": { + "@eslint/js": "^9.33.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.5.2", @@ -51,6 +52,7 @@ "eslint-plugin-vitest": "^0.5.4", "globals": "16.3.0", "jsdom": "^26.1.0", + "prettier": "^3.6.2", "typescript": "^5.2.2", "vite": "^7.0.2", "vite-plugin-eslint": "^1.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 093f3ec7..5292986e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: specifier: 19.1.0 version: 19.1.0(react@19.1.0) devDependencies: + '@eslint/js': + specifier: ^9.33.0 + version: 9.33.0 '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -86,7 +89,7 @@ importers: version: 2.32.0(@typescript-eslint/parser@8.35.0(eslint@9.30.0)(typescript@5.6.3))(eslint@9.30.0) eslint-plugin-prettier: specifier: ^5.5.1 - version: 5.5.1(@types/eslint@8.56.12)(eslint-config-prettier@10.1.5(eslint@9.30.0))(eslint@9.30.0)(prettier@3.3.3) + version: 5.5.1(@types/eslint@8.56.12)(eslint-config-prettier@10.1.5(eslint@9.30.0))(eslint@9.30.0)(prettier@3.6.2) eslint-plugin-react: specifier: ^7.37.0 version: 7.37.2(eslint@9.30.0) @@ -95,7 +98,7 @@ importers: version: 5.2.0(eslint@9.30.0) eslint-plugin-storybook: specifier: ^9.0.14 - version: 9.0.14(eslint@9.30.0)(storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.3.3))(typescript@5.6.3) + version: 9.0.14(eslint@9.30.0)(storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.6.3) eslint-plugin-vitest: specifier: ^0.5.4 version: 0.5.4(@typescript-eslint/eslint-plugin@8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.30.0)(typescript@5.6.3))(eslint@9.30.0)(typescript@5.6.3))(eslint@9.30.0)(typescript@5.6.3)(vitest@3.2.4) @@ -105,6 +108,9 @@ importers: jsdom: specifier: ^26.1.0 version: 26.1.0 + prettier: + specifier: ^3.6.2 + version: 3.6.2 typescript: specifier: ^5.2.2 version: 5.6.3 @@ -468,6 +474,10 @@ packages: resolution: {integrity: sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.33.0': + resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2562,8 +2572,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} hasBin: true @@ -3628,6 +3638,8 @@ snapshots: '@eslint/js@9.30.0': {} + '@eslint/js@9.33.0': {} + '@eslint/object-schema@2.1.6': {} '@eslint/plugin-kit@0.3.3': @@ -4958,10 +4970,10 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-prettier@5.5.1(@types/eslint@8.56.12)(eslint-config-prettier@10.1.5(eslint@9.30.0))(eslint@9.30.0)(prettier@3.3.3): + eslint-plugin-prettier@5.5.1(@types/eslint@8.56.12)(eslint-config-prettier@10.1.5(eslint@9.30.0))(eslint@9.30.0)(prettier@3.6.2): dependencies: eslint: 9.30.0 - prettier: 3.3.3 + prettier: 3.6.2 prettier-linter-helpers: 1.0.0 synckit: 0.11.8 optionalDependencies: @@ -4994,11 +5006,11 @@ snapshots: string.prototype.matchall: 4.0.11 string.prototype.repeat: 1.0.0 - eslint-plugin-storybook@9.0.14(eslint@9.30.0)(storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.3.3))(typescript@5.6.3): + eslint-plugin-storybook@9.0.14(eslint@9.30.0)(storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.6.3): dependencies: '@typescript-eslint/utils': 8.35.0(eslint@9.30.0)(typescript@5.6.3) eslint: 9.30.0 - storybook: 9.0.14(@testing-library/dom@10.4.0)(prettier@3.3.3) + storybook: 9.0.14(@testing-library/dom@10.4.0)(prettier@3.6.2) transitivePeerDependencies: - supports-color - typescript @@ -5984,7 +5996,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.3.3: {} + prettier@3.6.2: {} pretty-format@27.5.1: dependencies: @@ -6331,7 +6343,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.3.3): + storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.6.2): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.6.3 @@ -6345,7 +6357,7 @@ snapshots: semver: 7.6.3 ws: 8.18.0 optionalDependencies: - prettier: 3.3.3 + prettier: 3.6.2 transitivePeerDependencies: - '@testing-library/dom' - bufferutil From 804be867bff91f7b9b54aa7369d031da81e7e728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=92?= =?UTF-8?q?=E1=85=AE=E1=86=AB?= Date: Fri, 22 Aug 2025 01:36:21 +0900 Subject: [PATCH 3/7] =?UTF-8?q?Feat:=20easy=20test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/easy.useCalendarView.spec.ts | 85 +++- src/__tests__/hooks/easy.useSearch.spec.ts | 61 ++- src/__tests__/unit/easy.dateUtils.spec.ts | 367 +++++++++++++++--- src/__tests__/unit/easy.eventOverlap.spec.ts | 181 ++++++++- src/__tests__/unit/easy.eventUtils.spec.ts | 111 +++++- src/__tests__/unit/easy.fetchHolidays.spec.ts | 33 +- .../unit/easy.notificationUtils.spec.ts | 107 ++++- .../unit/easy.timeValidation.spec.ts | 42 +- 8 files changed, 886 insertions(+), 101 deletions(-) diff --git a/src/__tests__/hooks/easy.useCalendarView.spec.ts b/src/__tests__/hooks/easy.useCalendarView.spec.ts index 93b57f0e..3f9ea872 100644 --- a/src/__tests__/hooks/easy.useCalendarView.spec.ts +++ b/src/__tests__/hooks/easy.useCalendarView.spec.ts @@ -4,21 +4,88 @@ import { useCalendarView } from '../../hooks/useCalendarView.ts'; import { assertDate } from '../utils.ts'; describe('초기 상태', () => { - it('view는 "month"이어야 한다', () => {}); + it('view는 "month"이어야 한다', () => { + const { result } = renderHook(useCalendarView); + expect(result.current.view).toBe('month'); + }); - it('currentDate는 오늘 날짜인 "2025-10-01"이어야 한다', () => {}); + it('currentDate는 오늘 날짜인 "2025-10-01"이어야 한다', () => { + const { result } = renderHook(useCalendarView); + expect(result.current.currentDate).toEqual(new Date('2025-10-01')); + }); - it('holidays는 10월 휴일인 개천절, 한글날, 추석이 지정되어 있어야 한다', () => {}); + it('holidays는 10월 휴일인 개천절, 한글날, 추석이 지정되어 있어야 한다', () => { + const { result } = renderHook(useCalendarView); + expect(result.current.holidays).toHaveProperty('2025-10-05', '추석'); + expect(result.current.holidays).toHaveProperty('2025-10-03', '개천절'); + expect(result.current.holidays).toHaveProperty('2025-10-09', '한글날'); + }); }); -it("view를 'week'으로 변경 시 적절하게 반영된다", () => {}); +it("view를 'week'으로 변경 시 적절하게 반영된다", () => { + const { result } = renderHook(useCalendarView); -it("주간 뷰에서 다음으로 navigate시 7일 후 '2025-10-08' 날짜로 지정이 된다", () => {}); + act(() => { + result.current.setView('week'); + }); -it("주간 뷰에서 이전으로 navigate시 7일 후 '2025-09-24' 날짜로 지정이 된다", () => {}); + expect(result.current.view).toBe('week'); +}); + +it("주간 뷰에서 다음으로 navigate시 7일 후 '2025-10-08' 날짜로 지정이 된다", () => { + const { result } = renderHook(useCalendarView); + + act(() => { + result.current.setView('week'); + }); + + act(() => { + result.current.navigate('next'); + }); + + expect(result.current.currentDate).toEqual(new Date('2025-10-08')); +}); + +it("주간 뷰에서 이전으로 navigate시 7일 전 '2025-09-24' 날짜로 지정이 된다", () => { + const { result } = renderHook(useCalendarView); + + act(() => { + result.current.setView('week'); + }); + + act(() => { + result.current.navigate('prev'); + }); + + expect(result.current.currentDate).toEqual(new Date('2025-09-24')); +}); + +it("월간 뷰에서 다음으로 navigate시 한 달 후 '2025-11-01' 날짜여야 한다", () => { + const { result } = renderHook(useCalendarView); + + act(() => { + result.current.navigate('next'); + }); + + expect(result.current.currentDate).toEqual(new Date('2025-11-01')); +}); -it("월간 뷰에서 다음으로 navigate시 한 달 후 '2025-11-01' 날짜여야 한다", () => {}); +it("월간 뷰에서 이전으로 navigate시 한 달 전 '2025-09-01' 날짜여야 한다", () => { + const { result } = renderHook(useCalendarView); -it("월간 뷰에서 이전으로 navigate시 한 달 전 '2025-09-01' 날짜여야 한다", () => {}); + act(() => { + result.current.navigate('prev'); + }); -it("currentDate가 '2025-03-01' 변경되면 3월 휴일 '삼일절'로 업데이트되어야 한다", async () => {}); + expect(result.current.currentDate).toEqual(new Date('2025-09-01')); +}); + +it("currentDate가 '2025-03-01' 변경되면 3월 휴일 '삼일절'로 업데이트되어야 한다", async () => { + const { result } = renderHook(useCalendarView); + + act(() => { + result.current.setCurrentDate(new Date('2025-03-01')); + }); + + expect(result.current.holidays).toHaveProperty('2025-03-01', '삼일절'); +}); diff --git a/src/__tests__/hooks/easy.useSearch.spec.ts b/src/__tests__/hooks/easy.useSearch.spec.ts index 80f57fa3..7d3f3694 100644 --- a/src/__tests__/hooks/easy.useSearch.spec.ts +++ b/src/__tests__/hooks/easy.useSearch.spec.ts @@ -1,14 +1,65 @@ import { act, renderHook } from '@testing-library/react'; +import dummyEvents from '../../__mocks__/response/events.json'; import { useSearch } from '../../hooks/useSearch.ts'; import { Event } from '../../types.ts'; -it('검색어가 비어있을 때 모든 이벤트를 반환해야 한다', () => {}); +const events = dummyEvents.events as Event[]; -it('검색어에 맞는 이벤트만 필터링해야 한다', () => {}); +it('검색어가 비어있을 때 모든 이벤트를 반환해야 한다', () => { + const { result } = renderHook(() => useSearch(events, new Date(), 'month')); -it('검색어가 제목, 설명, 위치 중 하나라도 일치하면 해당 이벤트를 반환해야 한다', () => {}); + act(() => { + result.current.setSearchTerm(''); + }); -it('현재 뷰(주간/월간)에 해당하는 이벤트만 반환해야 한다', () => {}); + expect(result.current.filteredEvents).toEqual(events); +}); -it("검색어를 '회의'에서 '점심'으로 변경하면 필터링된 결과가 즉시 업데이트되어야 한다", () => {}); +it('검색어에 맞는 이벤트만 필터링해야 한다', () => { + const { result } = renderHook(() => useSearch(events, new Date(), 'month')); + + act(() => { + result.current.setSearchTerm('회의'); + }); + + expect(result.current.filteredEvents).toEqual([events[0]]); +}); + +it('검색어가 제목, 설명, 위치 중 하나라도 일치하면 해당 이벤트를 반환해야 한다', () => { + const { result } = renderHook(() => useSearch(events, new Date(), 'month')); + + act(() => { + result.current.setSearchTerm('회의실 B'); + }); + + console.log(result.current.filteredEvents); + + expect(result.current.filteredEvents).toEqual([events[0]]); +}); + +it('현재 뷰(주간/월간)에 해당하는 이벤트만 반환해야 한다', () => { + const { result } = renderHook(() => useSearch(events, new Date(), 'week')); + + act(() => { + result.current.setSearchTerm('기존 회의'); + }); + + expect(result.current.filteredEvents).toEqual([]); +}); + +it("검색어를 '회의'에서 '점심'으로 변경하면 필터링된 결과가 즉시 업데이트되어야 한다", () => { + const { result } = renderHook(() => useSearch(events, new Date(), 'month')); + + act(() => { + result.current.setSearchTerm('회의'); + }); + + expect(result.current.filteredEvents).toEqual([events[0]]); + + act(() => { + result.current.setSearchTerm('점심'); + }); + + expect(result.current.filteredEvents).toEqual([]); +}); diff --git a/src/__tests__/unit/easy.dateUtils.spec.ts b/src/__tests__/unit/easy.dateUtils.spec.ts index 967bfacd..46f21fbd 100644 --- a/src/__tests__/unit/easy.dateUtils.spec.ts +++ b/src/__tests__/unit/easy.dateUtils.spec.ts @@ -12,105 +12,364 @@ import { } from '../../utils/dateUtils'; describe('getDaysInMonth', () => { - it('1월은 31일 수를 반환한다', () => {}); - - it('4월은 30일 일수를 반환한다', () => {}); + it('1월은 31일 수를 반환한다', () => { + expect(getDaysInMonth(2025, 1)).toBe(31); + }); + + it('4월은 30일 일수를 반환한다', () => { + expect(getDaysInMonth(2025, 4)).toBe(30); + }); + + it('윤년의 2월에 대해 29일을 반환한다', () => { + expect(getDaysInMonth(2024, 2)).toBe(29); + }); + + it('평년의 2월에 대해 28일을 반환한다', () => { + expect(getDaysInMonth(2025, 2)).toBe(28); + }); + + // 무의미한 테스트 삭제 + it('유효하지 않은 월에 대해 적절히 처리한다', () => { + expect(getDaysInMonth(2025, 0)).toBe(31); // 작년 11월로 계산 + expect(getDaysInMonth(2025, 13)).toBe(31); // 익년 1월로 간주 + }); +}); - it('윤년의 2월에 대해 29일을 반환한다', () => {}); +describe('getWeekDates', () => { + it('주중의 날짜(수요일)에 대해 올바른 주의 날짜들을 반환한다', () => { + const currentDate = new Date(); // 2025년 10월 1일 (수요일) + const weekDates = getWeekDates(currentDate); + + expect(weekDates).toEqual([ + new Date(2025, 8, 28), // 일요일 + new Date(2025, 8, 29), // 월요일 + new Date(2025, 8, 30), // 화요일 + new Date(2025, 9, 1), // 수요일 + new Date(2025, 9, 2), // 목요일 + new Date(2025, 9, 3), // 금요일 + new Date(2025, 9, 4), // 토요일 + ]); + }); + + it('주의 시작(월요일)에 대해 올바른 주의 날짜들을 반환한다', () => { + const currentDate = new Date(2025, 8, 29); // 월요일 + const weekDates = getWeekDates(currentDate); + + expect(weekDates).toEqual([ + new Date(2025, 8, 28), // 일요일 + new Date(2025, 8, 29), // 월요일 + new Date(2025, 8, 30), // 화요일 + new Date(2025, 9, 1), // 수요일 + new Date(2025, 9, 2), // 목요일 + new Date(2025, 9, 3), // 금요일 + new Date(2025, 9, 4), // 토요일 + ]); + }); + + it('주의 끝(일요일)에 대해 올바른 주의 날짜들을 반환한다', () => { + const currentDate = new Date(2025, 8, 28); // 일요일 + const weekDates = getWeekDates(currentDate); + + expect(weekDates).toEqual([ + new Date(2025, 8, 28), // 일요일 + new Date(2025, 8, 29), // 월요일 + new Date(2025, 8, 30), // 화요일 + new Date(2025, 9, 1), // 수요일 + new Date(2025, 9, 2), // 목요일 + new Date(2025, 9, 3), // 금요일 + new Date(2025, 9, 4), // 토요일 + ]); + }); + + it('연도를 넘어가는 주의 날짜를 정확히 처리한다 (연말)', () => { + const currentDate = new Date(2025, 11, 31); // 2025년 12월 31일 (수요일) + const weekDates = getWeekDates(currentDate); + console.log(weekDates); + + expect(weekDates).toEqual([ + new Date(2025, 11, 28), // 일요일 + new Date(2025, 11, 29), // 월요일 + new Date(2025, 11, 30), // 화요일 + new Date(2025, 11, 31), // 수요일 + new Date(2026, 0, 1), // 목요일 + new Date(2026, 0, 2), // 금요일 + new Date(2026, 0, 3), // 토요일 + ]); + }); + + it('연도를 넘어가는 주의 날짜를 정확히 처리한다 (연초)', () => { + const currentDate = new Date(2026, 0, 1); // 2026년 1월 1일 (목요일) + const weekDates = getWeekDates(currentDate); + console.log(weekDates); + + expect(weekDates).toEqual([ + new Date(2025, 11, 28), // 일요일 + new Date(2025, 11, 29), // 월요일 + new Date(2025, 11, 30), // 화요일 + new Date(2025, 11, 31), // 수요일 + new Date(2026, 0, 1), // 목요일 + new Date(2026, 0, 2), // 금요일 + new Date(2026, 0, 3), // 토요일 + ]); + }); + + it('윤년의 2월 29일을 포함한 주를 올바르게 처리한다', () => { + const currentDate = new Date(2024, 1, 29); // 2024년 2월 29일 (목요일) + const weekDates = getWeekDates(currentDate); + console.log(weekDates); + + expect(weekDates).toEqual([ + new Date(2024, 1, 25), // 일요일 + new Date(2024, 1, 26), // 월요일 + new Date(2024, 1, 27), // 화요일 + new Date(2024, 1, 28), // 수요일 + new Date(2024, 1, 29), // 목요일 + new Date(2024, 2, 1), // 금요일 + new Date(2024, 2, 2), // 토요일 + ]); + }); + + it('월의 마지막 날짜를 포함한 주를 올바르게 처리한다', () => { + const currentDate = new Date(2025, 11, 31); // 2025년 12월 31일 (수요일) + const weekDates = getWeekDates(currentDate); + console.log(weekDates); + + expect(weekDates).toEqual([ + new Date(2025, 11, 28), // 일요일 + new Date(2025, 11, 29), // 월요일 + new Date(2025, 11, 30), // 화요일 + new Date(2025, 11, 31), // 수요일 + new Date(2026, 0, 1), // 목요일 + new Date(2026, 0, 2), // 금요일 + new Date(2026, 0, 3), // 토요일 + ]); + }); +}); - it('평년의 2월에 대해 28일을 반환한다', () => {}); +describe('getWeeksAtMonth', () => { + it('2025년 7월 1일의 올바른 주 정보를 반환해야 한다', () => { + const currentDate = new Date(2025, 6, 1); // 2025년 7월 1일 (화요일) + const weeks = getWeeksAtMonth(currentDate); + console.log(weeks); + + expect(weeks).toEqual([ + [null, null, 1, 2, 3, 4, 5], + [6, 7, 8, 9, 10, 11, 12], + [13, 14, 15, 16, 17, 18, 19], + [20, 21, 22, 23, 24, 25, 26], + [27, 28, 29, 30, 31, null, null], + ]); + }); +}); - it('유효하지 않은 월에 대해 적절히 처리한다', () => {}); +describe('getEventsForDay', () => { + const dummyEvents = [ + { + id: '1', + title: '기존 회의', + date: '2025-10-1', + startTime: '09:00', + endTime: '10:00', + description: '기존 팀 미팅', + location: '회의실 B', + category: '업무', + repeat: { type: 'none', interval: 0 }, + notificationTime: 10, + }, + ] as Event[]; + + it('특정 날짜(1일)에 해당하는 이벤트만 정확히 반환한다', () => { + const events = getEventsForDay(dummyEvents, 1); + + expect(events).toEqual(dummyEvents); + }); + + it('해당 날짜에 이벤트가 없을 경우 빈 배열을 반환한다', () => { + const events = getEventsForDay(dummyEvents, 2); + console.log(events); + + expect(events).toEqual([]); + }); + + it('날짜가 0일 경우 빈 배열을 반환한다', () => { + const events = getEventsForDay(dummyEvents, 0); + console.log(events); + + expect(events).toEqual([]); + }); + + it('날짜가 32일 이상인 경우 빈 배열을 반환한다', () => { + const events = getEventsForDay(dummyEvents, 32); + console.log(events); + + expect(events).toEqual([]); + }); }); -describe('getWeekDates', () => { - it('주중의 날짜(수요일)에 대해 올바른 주의 날짜들을 반환한다', () => {}); +describe('formatWeek', () => { + it('월의 중간 날짜에 대해 올바른 주 정보를 반환한다', () => { + const currentDate = new Date(); - it('주의 시작(월요일)에 대해 올바른 주의 날짜들을 반환한다', () => {}); + expect(formatWeek(currentDate)).toBe('2025년 10월 1주'); + }); - it('주의 끝(일요일)에 대해 올바른 주의 날짜들을 반환한다', () => {}); + it('월의 첫 주에 대해 올바른 주 정보를 반환한다', () => { + const currentDate = new Date(2025, 9, 1); // 2025년 10월 1일 (수요일) - it('연도를 넘어가는 주의 날짜를 정확히 처리한다 (연말)', () => {}); + expect(formatWeek(currentDate)).toBe('2025년 10월 1주'); + }); - it('연도를 넘어가는 주의 날짜를 정확히 처리한다 (연초)', () => {}); + it('월의 마지막 주에 대해 올바른 주 정보를 반환한다', () => { + const currentDate = new Date(2025, 9, 31); // 2025년 10월 31일 (금요일) - it('윤년의 2월 29일을 포함한 주를 올바르게 처리한다', () => {}); + expect(formatWeek(currentDate)).toBe('2025년 10월 5주'); + }); - it('월의 마지막 날짜를 포함한 주를 올바르게 처리한다', () => {}); -}); + it('연도가 바뀌는 주에 대해 올바른 주 정보를 반환한다', () => { + const currentDate = new Date(2025, 11, 31); // 2025년 12월 31일 (수요일) + const weekInfo = formatWeek(currentDate); -describe('getWeeksAtMonth', () => { - it('2025년 7월 1일의 올바른 주 정보를 반환해야 한다', () => {}); -}); + expect(weekInfo).toBe('2026년 1월 1주'); + }); -describe('getEventsForDay', () => { - it('특정 날짜(1일)에 해당하는 이벤트만 정확히 반환한다', () => {}); + it('윤년 2월의 마지막 주에 대해 올바른 주 정보를 반환한다', () => { + const currentDate = new Date(2024, 1, 29); // 2024년 2월 29일 (목요일) + const weekInfo = formatWeek(currentDate); - it('해당 날짜에 이벤트가 없을 경우 빈 배열을 반환한다', () => {}); + expect(weekInfo).toBe('2024년 2월 5주'); + }); - it('날짜가 0일 경우 빈 배열을 반환한다', () => {}); + it('평년 2월의 마지막 주에 대해 올바른 주 정보를 반환한다', () => { + const currentDate = new Date(2025, 1, 28); // 2025년 2월 28일 (화요일) + const weekInfo = formatWeek(currentDate); - it('날짜가 32일 이상인 경우 빈 배열을 반환한다', () => {}); + expect(weekInfo).toBe('2025년 2월 4주'); + }); }); -describe('formatWeek', () => { - it('월의 중간 날짜에 대해 올바른 주 정보를 반환한다', () => {}); +describe('formatMonth', () => { + it("2025년 7월 10일을 '2025년 7월'로 반환한다", () => { + const currentDate = new Date(2025, 6, 10); // 2025년 7월 10일 (목요일) + const monthInfo = formatMonth(currentDate); - it('월의 첫 주에 대해 올바른 주 정보를 반환한다', () => {}); + expect(monthInfo).toBe('2025년 7월'); + }); +}); - it('월의 마지막 주에 대해 올바른 주 정보를 반환한다', () => {}); +describe('isDateInRange', () => { + it('범위 내의 날짜 2025-07-10에 대해 true를 반환한다', () => { + const startDate = new Date(2025, 6, 1); + const endDate = new Date(2025, 6, 31); + const testDate = new Date(2025, 6, 10); - it('연도가 바뀌는 주에 대해 올바른 주 정보를 반환한다', () => {}); + expect(isDateInRange(testDate, startDate, endDate)).toBe(true); + }); - it('윤년 2월의 마지막 주에 대해 올바른 주 정보를 반환한다', () => {}); + it('범위의 시작일 2025-07-01에 대해 true를 반환한다', () => { + const startDate = new Date(2025, 6, 1); + const endDate = new Date(2025, 6, 31); + const testDate = new Date(2025, 6, 1); - it('평년 2월의 마지막 주에 대해 올바른 주 정보를 반환한다', () => {}); -}); + expect(isDateInRange(testDate, startDate, endDate)).toBe(true); + }); -describe('formatMonth', () => { - it("2025년 7월 10일을 '2025년 7월'로 반환한다", () => {}); -}); + it('범위의 종료일 2025-07-31에 대해 true를 반환한다', () => { + const startDate = new Date(2025, 6, 1); + const endDate = new Date(2025, 6, 31); + const testDate = new Date(2025, 6, 31); -describe('isDateInRange', () => { - it('범위 내의 날짜 2025-07-10에 대해 true를 반환한다', () => {}); + expect(isDateInRange(testDate, startDate, endDate)).toBe(true); + }); + + it('범위 이전의 날짜 2025-06-30에 대해 false를 반환한다', () => { + const startDate = new Date(2025, 6, 1); + const endDate = new Date(2025, 6, 31); + const testDate = new Date(2025, 5, 30); - it('범위의 시작일 2025-07-01에 대해 true를 반환한다', () => {}); + expect(isDateInRange(testDate, startDate, endDate)).toBe(false); + }); - it('범위의 종료일 2025-07-31에 대해 true를 반환한다', () => {}); + it('범위 이후의 날짜 2025-08-01에 대해 false를 반환한다', () => { + const startDate = new Date(2025, 6, 1); + const endDate = new Date(2025, 6, 31); + const testDate = new Date(2025, 7, 1); - it('범위 이전의 날짜 2025-06-30에 대해 false를 반환한다', () => {}); + expect(isDateInRange(testDate, startDate, endDate)).toBe(false); + }); - it('범위 이후의 날짜 2025-08-01에 대해 false를 반환한다', () => {}); + it('시작일이 종료일보다 늦은 경우 모든 날짜에 대해 false를 반환한다', () => { + const startDate = new Date(2025, 6, 31); + const endDate = new Date(2025, 6, 1); + const testDate = new Date(2025, 6, 15); - it('시작일이 종료일보다 늦은 경우 모든 날짜에 대해 false를 반환한다', () => {}); + expect(isDateInRange(testDate, startDate, endDate)).toBe(false); + }); }); describe('fillZero', () => { - it("5를 2자리로 변환하면 '05'를 반환한다", () => {}); + it("5를 2자리로 변환하면 '05'를 반환한다", () => { + expect(fillZero(5, 2)).toBe('05'); + }); - it("10을 2자리로 변환하면 '10'을 반환한다", () => {}); + it("10을 2자리로 변환하면 '10'을 반환한다", () => { + expect(fillZero(10, 2)).toBe('10'); + }); - it("3을 3자리로 변환하면 '003'을 반환한다", () => {}); + it("3을 3자리로 변환하면 '003'을 반환한다", () => { + expect(fillZero(3, 3)).toBe('003'); + }); - it("100을 2자리로 변환하면 '100'을 반환한다", () => {}); + it("100을 2자리로 변환하면 '100'을 반환한다", () => { + expect(fillZero(100, 2)).toBe('100'); + }); - it("0을 2자리로 변환하면 '00'을 반환한다", () => {}); + it("0을 2자리로 변환하면 '00'을 반환한다", () => { + expect(fillZero(0, 2)).toBe('00'); + }); - it("1을 5자리로 변환하면 '00001'을 반환한다", () => {}); + it("1을 5자리로 변환하면 '00001'을 반환한다", () => { + expect(fillZero(1, 5)).toBe('00001'); + }); - it("소수점이 있는 3.14를 5자리로 변환하면 '03.14'를 반환한다", () => {}); + it("소수점이 있는 3.14를 5자리로 변환하면 '03.14'를 반환한다", () => { + expect(fillZero(3.14, 5)).toBe('03.14'); + }); - it('size 파라미터를 생략하면 기본값 2를 사용한다', () => {}); + it('size 파라미터를 생략하면 기본값 2를 사용한다', () => { + expect(fillZero(5)).toBe('05'); + }); - it('value가 지정된 size보다 큰 자릿수를 가지면 원래 값을 그대로 반환한다', () => {}); + it('value가 지정된 size보다 큰 자릿수를 가지면 원래 값을 그대로 반환한다', () => { + expect(fillZero(100, 2)).toBe('100'); + }); }); describe('formatDate', () => { - it('날짜를 YYYY-MM-DD 형식으로 포맷팅한다', () => {}); + it('날짜를 YYYY-MM-DD 형식으로 포맷팅한다', () => { + const currentDate = new Date(); + const formattedDate = formatDate(currentDate); + + expect(formattedDate).toBe('2025-10-01'); + }); + + it('day 파라미터가 제공되면 해당 일자로 포맷팅한다', () => { + const currentDate = new Date(); + const formattedDate = formatDate(currentDate, 15); + expect(formattedDate).toBe('2025-10-15'); + }); + + it('월이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => { + const currentDate = new Date(2025, 4, 1); + const formattedDate = formatDate(currentDate); - it('day 파라미터가 제공되면 해당 일자로 포맷팅한다', () => {}); + console.log(formattedDate); + expect(formattedDate).toBe('2025-05-01'); + }); - it('월이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => {}); + it('일이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => { + const currentDate = new Date(); + const formattedDate = formatDate(currentDate, 5); - it('일이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => {}); + expect(formattedDate).toBe('2025-10-05'); + }); }); diff --git a/src/__tests__/unit/easy.eventOverlap.spec.ts b/src/__tests__/unit/easy.eventOverlap.spec.ts index 5e5f6497..ed865e71 100644 --- a/src/__tests__/unit/easy.eventOverlap.spec.ts +++ b/src/__tests__/unit/easy.eventOverlap.spec.ts @@ -5,32 +5,191 @@ import { isOverlapping, parseDateTime, } from '../../utils/eventOverlap'; + describe('parseDateTime', () => { - it('2025-07-01 14:30을 정확한 Date 객체로 변환한다', () => {}); + it('2025-07-01 14:30을 정확한 Date 객체로 변환한다', () => { + const date = '2025-07-01'; + const time = '14:30'; + const expectedDate = new Date('2025-07-01T14:30'); + + expect(parseDateTime(date, time)).toEqual(expectedDate); + }); + + it('잘못된 날짜 형식에 대해 Invalid Date를 반환한다', () => { + const date = '2025-07-32'; + const time = '14:30'; + const result = parseDateTime(date, time); + + expect(result).toEqual(new Date('Invalid Date')); + }); + + it('잘못된 시간 형식에 대해 Invalid Date를 반환한다', () => { + const date = '2025-07-01'; + const time = '25:30'; + const result = parseDateTime(date, time); - it('잘못된 날짜 형식에 대해 Invalid Date를 반환한다', () => {}); + expect(result).toEqual(new Date('Invalid Date')); + }); - it('잘못된 시간 형식에 대해 Invalid Date를 반환한다', () => {}); + it('날짜 문자열이 비어있을 때 Invalid Date를 반환한다', () => { + const date = ''; + const time = '14:30'; + const result = parseDateTime(date, time); - it('날짜 문자열이 비어있을 때 Invalid Date를 반환한다', () => {}); + expect(result).toEqual(new Date('Invalid Date')); + }); }); describe('convertEventToDateRange', () => { - it('일반적인 이벤트를 올바른 시작 및 종료 시간을 가진 객체로 변환한다', () => {}); + it('일반적인 이벤트를 올바른 시작 및 종료 시간을 가진 객체로 변환한다', () => { + const event = { + id: '1', + title: 'Test Event', + date: '2025-07-01', + startTime: '14:30', + endTime: '16:00', + description: '', + location: '', + } as Event; + + const expectedRange = { + start: new Date('2025-07-01T14:30'), + end: new Date('2025-07-01T16:00'), + }; + + expect(convertEventToDateRange(event)).toEqual(expectedRange); + }); + + it('잘못된 날짜 형식의 이벤트에 대해 Invalid Date를 반환한다', () => { + const event = { + id: '2', + title: 'Invalid Date Event', + date: '2025-07-32', + startTime: '14:30', + endTime: '16:00', + description: '', + location: '', + } as Event; + + const expectedRange = { + start: new Date('Invalid Date'), + end: new Date('Invalid Date'), + }; + + expect(convertEventToDateRange(event)).toEqual(expectedRange); + }); - it('잘못된 날짜 형식의 이벤트에 대해 Invalid Date를 반환한다', () => {}); + it('잘못된 시간 형식의 이벤트에 대해 Invalid Date를 반환한다', () => { + const event = { + id: '3', + title: 'Invalid Time Event', + date: '2025-07-01', + startTime: '25:30', + endTime: '26:00', + description: '', + location: '', + } as Event; - it('잘못된 시간 형식의 이벤트에 대해 Invalid Date를 반환한다', () => {}); + const expectedRange = { + start: new Date('Invalid Date'), + end: new Date('Invalid Date'), + }; + + expect(convertEventToDateRange(event)).toEqual(expectedRange); + }); }); describe('isOverlapping', () => { - it('두 이벤트가 겹치는 경우 true를 반환한다', () => {}); + it('두 이벤트가 겹치는 경우 true를 반환한다', () => { + const event1 = { + id: '1', + date: '2025-07-01', + startTime: '14:00', + endTime: '15:00', + } as Event; + + const event2 = { + id: '2', + date: '2025-07-01', + startTime: '14:30', + endTime: '15:30', + } as Event; + + expect(isOverlapping(event1, event2)).toBe(true); + }); + + it('두 이벤트가 겹치지 않는 경우 false를 반환한다', () => { + const event1 = { + id: '1', + date: '2025-07-01', + startTime: '14:00', + endTime: '15:00', + } as Event; + + const event2 = { + id: '2', + date: '2025-07-01', + startTime: '15:30', + endTime: '16:30', + } as Event; - it('두 이벤트가 겹치지 않는 경우 false를 반환한다', () => {}); + expect(isOverlapping(event1, event2)).toBe(false); + }); }); describe('findOverlappingEvents', () => { - it('새 이벤트와 겹치는 모든 이벤트를 반환한다', () => {}); + it('새 이벤트와 겹치는 모든 이벤트를 반환한다', () => { + const newEvent = { + id: '1', + date: '2025-07-01', + startTime: '14:30', + endTime: '15:30', + } as Event; + + const existingEvents = [ + { + id: '2', + date: '2025-07-01', + startTime: '14:00', + endTime: '15:00', + } as Event, + { + id: '3', + date: '2025-07-01', + startTime: '15:30', + endTime: '16:30', + } as Event, + ]; + + const overlappingEvents = findOverlappingEvents(newEvent, existingEvents); + console.log(overlappingEvents, existingEvents); + expect(overlappingEvents).toEqual([existingEvents[0]]); + }); + + it('겹치는 이벤트가 없으면 빈 배열을 반환한다', () => { + const newEvent = { + id: '4', + date: '2025-07-01', + startTime: '16:30', + endTime: '17:30', + } as Event; + + const existingEvents = [ + { + id: '2', + date: '2025-07-01', + startTime: '14:00', + endTime: '15:00', + } as Event, + { + id: '3', + date: '2025-07-01', + startTime: '15:30', + endTime: '16:30', + } as Event, + ]; - it('겹치는 이벤트가 없으면 빈 배열을 반환한다', () => {}); + const overlappingEvents = findOverlappingEvents(newEvent, existingEvents); + expect(overlappingEvents).toEqual([]); + }); }); diff --git a/src/__tests__/unit/easy.eventUtils.spec.ts b/src/__tests__/unit/easy.eventUtils.spec.ts index 8eef6371..ab04e6a4 100644 --- a/src/__tests__/unit/easy.eventUtils.spec.ts +++ b/src/__tests__/unit/easy.eventUtils.spec.ts @@ -1,20 +1,115 @@ import { Event } from '../../types'; import { getFilteredEvents } from '../../utils/eventUtils'; +const events = [ + { + id: '1', + title: '이벤트 1', + date: '2025-07-01', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '2', + title: '이벤트 2', + date: '2025-07-02', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '3', + title: '이벤트 3', + date: '2025-07-31', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '4', + title: 'EvEnT 4', + date: '2025-07-31', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '5', + title: 'EvEnT 5', + date: '2025-08-01', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, +] as Event[]; + describe('getFilteredEvents', () => { - it("검색어 '이벤트 2'에 맞는 이벤트만 반환한다", () => {}); + it("검색어 '이벤트 2'에 맞는 이벤트만 반환한다", () => { + const filteredEvents = getFilteredEvents(events, '이벤트 2', new Date('2025-07-01'), 'month'); + + expect(filteredEvents).toEqual([events[1]]); + }); + + it('주간 뷰에서 2025-07-01 주의 이벤트만 반환한다', () => { + const filteredEvents = getFilteredEvents(events, '', new Date('2025-07-01'), 'week'); + + expect(filteredEvents).toEqual([events[0], events[1]]); + }); + + it('월간 뷰에서 2025년 7월의 모든 이벤트를 반환한다', () => { + const filteredEvents = getFilteredEvents(events, '', new Date('2025-07-01'), 'month'); + + expect(filteredEvents).toEqual([events[0], events[1], events[2], events[3]]); + }); + + it("검색어 '이벤트'와 주간 뷰 필터링을 동시에 적용한다", () => { + const filteredEvents = getFilteredEvents(events, '이벤트', new Date('2025-07-01'), 'week'); + + expect(filteredEvents).toEqual([events[0], events[1]]); + }); + + it('검색어가 없을 때 모든 이벤트를 반환한다', () => { + const filteredEvents = getFilteredEvents(events, '', new Date('2025-07-01'), 'month'); - it('주간 뷰에서 2025-07-01 주의 이벤트만 반환한다', () => {}); + expect(filteredEvents).toEqual([events[0], events[1], events[2], events[3]]); + }); - it('월간 뷰에서 2025년 7월의 모든 이벤트를 반환한다', () => {}); + it('검색어가 대소문자를 구분하지 않고 작동한다', () => { + const filteredEvents = getFilteredEvents(events, 'event', new Date('2025-07-01'), 'month'); - it("검색어 '이벤트'와 주간 뷰 필터링을 동시에 적용한다", () => {}); + expect(filteredEvents).toEqual([events[3]]); + }); - it('검색어가 없을 때 모든 이벤트를 반환한다', () => {}); + it('월의 경계에 있는 이벤트를 올바르게 필터링한다', () => { + const filteredEvents = getFilteredEvents(events, '', new Date('2025-07-31'), 'month'); - it('검색어가 대소문자를 구분하지 않고 작동한다', () => {}); + expect(filteredEvents).toEqual([events[0], events[1], events[2], events[3]]); + }); - it('월의 경계에 있는 이벤트를 올바르게 필터링한다', () => {}); + it('빈 이벤트 리스트에 대해 빈 배열을 반환한다', () => { + const filteredEvents = getFilteredEvents([], '', new Date('2025-07-01'), 'month'); - it('빈 이벤트 리스트에 대해 빈 배열을 반환한다', () => {}); + expect(filteredEvents).toEqual([]); + }); }); diff --git a/src/__tests__/unit/easy.fetchHolidays.spec.ts b/src/__tests__/unit/easy.fetchHolidays.spec.ts index 013e87f0..a777c5fe 100644 --- a/src/__tests__/unit/easy.fetchHolidays.spec.ts +++ b/src/__tests__/unit/easy.fetchHolidays.spec.ts @@ -1,8 +1,35 @@ import { fetchHolidays } from '../../apis/fetchHolidays'; + describe('fetchHolidays', () => { - it('주어진 월의 공휴일만 반환한다', () => {}); + it('주어진 월의 공휴일만 반환한다', () => { + const result = fetchHolidays(new Date('2025-10-01')); + + console.log(result); + + expect(result).toEqual({ + '2025-10-05': '추석', + '2025-10-06': '추석', + '2025-10-07': '추석', + '2025-10-03': '개천절', + '2025-10-09': '한글날', + }); + }); + + it('공휴일이 없는 월에 대해 빈 객체를 반환한다', () => { + const result = fetchHolidays(new Date('2025-11-01')); + + expect(result).toEqual({}); + }); - it('공휴일이 없는 월에 대해 빈 객체를 반환한다', () => {}); + it('여러 공휴일이 있는 월에 대해 모든 공휴일을 반환한다', () => { + const result = fetchHolidays(new Date('2025-10-01')); - it('여러 공휴일이 있는 월에 대해 모든 공휴일을 반환한다', () => {}); + expect(result).toEqual({ + '2025-10-05': '추석', + '2025-10-06': '추석', + '2025-10-07': '추석', + '2025-10-03': '개천절', + '2025-10-09': '한글날', + }); + }); }); diff --git a/src/__tests__/unit/easy.notificationUtils.spec.ts b/src/__tests__/unit/easy.notificationUtils.spec.ts index 2fe10360..d2c135e4 100644 --- a/src/__tests__/unit/easy.notificationUtils.spec.ts +++ b/src/__tests__/unit/easy.notificationUtils.spec.ts @@ -1,16 +1,113 @@ import { Event } from '../../types'; import { createNotificationMessage, getUpcomingEvents } from '../../utils/notificationUtils'; +const events = [ + { + id: '1', + title: '이벤트 1', + date: '2025-07-01', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '2', + title: '이벤트 2', + date: '2025-07-02', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '3', + title: '이벤트 3', + date: '2025-07-31', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '4', + title: 'EvEnT 4', + date: '2025-07-31', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '5', + title: 'EvEnT 5', + date: '2025-08-01', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, +] as Event[]; + describe('getUpcomingEvents', () => { - it('알림 시간이 정확히 도래한 이벤트를 반환한다', () => {}); + it('알림 시간이 정확히 도래한 이벤트를 반환한다', () => { + const result = getUpcomingEvents(events, new Date('2025-07-01T13:59:00'), []); + + console.log(result); + + expect(result).toEqual([events[0]]); + }); + + it('이미 알림이 간 이벤트는 제외한다', () => { + const result = getUpcomingEvents(events, new Date('2025-07-01T13:59:00'), ['1']); + + console.log(result); + + expect(result).toEqual([]); + }); + + it('알림 시간이 아직 도래하지 않은 이벤트는 반환하지 않는다', () => { + const result = getUpcomingEvents(events, new Date('2025-07-01T13:00:00'), []); - it('이미 알림이 간 이벤트는 제외한다', () => {}); + console.log(result); - it('알림 시간이 아직 도래하지 않은 이벤트는 반환하지 않는다', () => {}); + expect(result).toEqual([]); + }); - it('알림 시간이 지난 이벤트는 반환하지 않는다', () => {}); + it('알림 시간이 지난 이벤트는 반환하지 않는다', () => { + const result = getUpcomingEvents(events, new Date('2025-07-01T14:01:00'), []); + + console.log(result); + + expect(result).toEqual([]); + }); }); describe('createNotificationMessage', () => { - it('올바른 알림 메시지를 생성해야 한다', () => {}); + it('올바른 알림 메시지를 생성해야 한다', () => { + const event = events[0]; + + const { notificationTime, title } = event; + + const result = createNotificationMessage(event); + + console.log(result); + + expect(result).toBe(`${notificationTime}분 후 ${title} 일정이 시작됩니다.`); + }); }); diff --git a/src/__tests__/unit/easy.timeValidation.spec.ts b/src/__tests__/unit/easy.timeValidation.spec.ts index 9dda1954..a61c8fa7 100644 --- a/src/__tests__/unit/easy.timeValidation.spec.ts +++ b/src/__tests__/unit/easy.timeValidation.spec.ts @@ -1,15 +1,45 @@ import { getTimeErrorMessage } from '../../utils/timeValidation'; describe('getTimeErrorMessage >', () => { - it('시작 시간이 종료 시간보다 늦을 때 에러 메시지를 반환한다', () => {}); + it('시작 시간이 종료 시간보다 늦을 때 에러 메시지를 반환한다', () => { + const result = getTimeErrorMessage('12:00', '11:00'); - it('시작 시간과 종료 시간이 같을 때 에러 메시지를 반환한다', () => {}); + expect(result.startTimeError).toBe('시작 시간은 종료 시간보다 빨라야 합니다.'); + expect(result.endTimeError).toBe('종료 시간은 시작 시간보다 늦어야 합니다.'); + }); - it('시작 시간이 종료 시간보다 빠를 때 null을 반환한다', () => {}); + it('시작 시간과 종료 시간이 같을 때 에러 메시지를 반환한다', () => { + const result = getTimeErrorMessage('10:00', '10:00'); - it('시작 시간이 비어있을 때 null을 반환한다', () => {}); + expect(result.startTimeError).toBe('시작 시간은 종료 시간보다 빨라야 합니다.'); + expect(result.endTimeError).toBe('종료 시간은 시작 시간보다 늦어야 합니다.'); + }); - it('종료 시간이 비어있을 때 null을 반환한다', () => {}); + it('시작 시간이 종료 시간보다 빠를 때 null을 반환한다', () => { + const result = getTimeErrorMessage('09:00', '10:00'); - it('시작 시간과 종료 시간이 모두 비어있을 때 null을 반환한다', () => {}); + expect(result.startTimeError).toBeNull(); + expect(result.endTimeError).toBeNull(); + }); + + it('시작 시간이 비어있을 때 null을 반환한다', () => { + const result = getTimeErrorMessage('', '10:00'); + + expect(result.startTimeError).toBeNull(); + expect(result.endTimeError).toBeNull(); + }); + + it('종료 시간이 비어있을 때 null을 반환한다', () => { + const result = getTimeErrorMessage('10:00', ''); + + expect(result.startTimeError).toBeNull(); + expect(result.endTimeError).toBeNull(); + }); + + it('시작 시간과 종료 시간이 모두 비어있을 때 null을 반환한다', () => { + const result = getTimeErrorMessage('', ''); + + expect(result.startTimeError).toBeNull(); + expect(result.endTimeError).toBeNull(); + }); }); From 429adf57744f45839f9d07b1c8fa5772aae25fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=92?= =?UTF-8?q?=E1=85=AE=E1=86=AB?= Date: Fri, 22 Aug 2025 13:37:14 +0900 Subject: [PATCH 4/7] =?UTF-8?q?Test:=20hooks=20test=20=EC=9E=91=EC=84=B1.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__mocks__/handlersUtils.ts | 38 ++++ .../hooks/medium.useEventOperations.spec.ts | 174 +++++++++++++++++- .../hooks/medium.useNotifications.spec.ts | 90 ++++++++- 3 files changed, 289 insertions(+), 13 deletions(-) diff --git a/src/__mocks__/handlersUtils.ts b/src/__mocks__/handlersUtils.ts index 0263c669..15a1f2c8 100644 --- a/src/__mocks__/handlersUtils.ts +++ b/src/__mocks__/handlersUtils.ts @@ -57,6 +57,10 @@ export const setupMockHandlerUpdating = () => { const updatedEvent = (await request.json()) as Event; const index = mockEvents.findIndex((event) => event.id === id); + if (index === -1) { + return HttpResponse.json({ error: 'Event not found' }, { status: 404 }); + } + mockEvents[index] = { ...mockEvents[index], ...updatedEvent }; return HttpResponse.json(mockEvents[index]); }) @@ -92,3 +96,37 @@ export const setupMockHandlerDeletion = () => { }) ); }; + +export const setupMockHandlerCreationFailure = () => { + server.use( + http.post('/api/events', async () => { + return HttpResponse.json(null, { status: 500 }); + }) + ); +}; + +export const setupMockHandlerDeletionFailure = () => { + const mockEvents: Event[] = [ + { + id: '1', + title: '삭제할 이벤트', + date: '2025-10-15', + startTime: '09:00', + endTime: '10:00', + description: '삭제할 이벤트입니다', + location: '어딘가', + category: '기타', + repeat: { type: 'none', interval: 0 }, + notificationTime: 10, + }, + ]; + + server.use( + http.get('/api/events', () => { + return HttpResponse.json({ events: mockEvents }); + }), + http.delete('/api/events/:id', ({ params }) => { + return new HttpResponse({ error: 'Network error' }, { status: 500 }); + }) + ); +}; diff --git a/src/__tests__/hooks/medium.useEventOperations.spec.ts b/src/__tests__/hooks/medium.useEventOperations.spec.ts index 566ecbb0..8c0829c8 100644 --- a/src/__tests__/hooks/medium.useEventOperations.spec.ts +++ b/src/__tests__/hooks/medium.useEventOperations.spec.ts @@ -1,13 +1,13 @@ import { act, renderHook } from '@testing-library/react'; -import { http, HttpResponse } from 'msw'; import { setupMockHandlerCreation, + setupMockHandlerCreationFailure, setupMockHandlerDeletion, + setupMockHandlerDeletionFailure, setupMockHandlerUpdating, } from '../../__mocks__/handlersUtils.ts'; import { useEventOperations } from '../../hooks/useEventOperations.ts'; -import { server } from '../../setupTests.ts'; import { Event } from '../../types.ts'; const enqueueSnackbarFn = vi.fn(); @@ -22,16 +22,172 @@ vi.mock('notistack', async () => { }; }); -it('저장되어있는 초기 이벤트 데이터를 적절하게 불러온다', async () => {}); +const events = [ + { + id: '1', + title: '이벤트 1', + date: '2025-07-01', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '2', + title: '이벤트 2', + date: '2025-07-02', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, +] as Event[]; -it('정의된 이벤트 정보를 기준으로 적절하게 저장이 된다', async () => {}); +it('저장되어있는 초기 이벤트 데이터를 적절하게 불러온다', async () => { + setupMockHandlerCreation(events); -it("새로 정의된 'title', 'endTime' 기준으로 적절하게 일정이 업데이트 된다", async () => {}); + const { result } = renderHook(() => useEventOperations(true)); -it('존재하는 이벤트 삭제 시 에러없이 아이템이 삭제된다.', async () => {}); + await act(async () => { + await result.current.fetchEvents(); + }); -it("이벤트 로딩 실패 시 '이벤트 로딩 실패'라는 텍스트와 함께 에러 토스트가 표시되어야 한다", async () => {}); + expect(result.current.events).toEqual(events); +}); + +it('정의된 이벤트 정보를 기준으로 적절하게 저장이 된다', async () => { + setupMockHandlerCreation(events); + + const { result } = renderHook(() => useEventOperations(false)); + + const newEvent = { + id: '3', + title: '이벤트 3', + date: '2025-07-02', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + } as Event; + + await act(async () => { + await result.current.saveEvent(newEvent); + }); + + expect(result.current.events).toContainEqual(newEvent); +}); + +it("새로 정의된 'title', 'endTime' 기준으로 적절하게 일정이 업데이트 된다", async () => { + setupMockHandlerUpdating(); + + const { result } = renderHook(() => useEventOperations(true)); + + await act(async () => { + await result.current.fetchEvents(); + }); + + const eventToUpdate = { + id: '2', + title: '이벤트 2 new', + date: '2025-07-03', + startTime: '15:00', + endTime: '16:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + } as Event; + + await act(async () => { + await result.current.saveEvent(eventToUpdate); + }); + + expect(result.current.events).toContainEqual(eventToUpdate); +}); + +it('존재하는 이벤트 삭제 시 에러없이 아이템이 삭제된다.', async () => { + setupMockHandlerDeletion(); + + const { result } = renderHook(() => useEventOperations(false)); + + await act(async () => { + await result.current.fetchEvents(); + }); + + const eventToDelete = result.current.events[0]; + + await act(async () => { + await result.current.deleteEvent(eventToDelete.id); + }); -it("존재하지 않는 이벤트 수정 시 '일정 저장 실패'라는 토스트가 노출되며 에러 처리가 되어야 한다", async () => {}); + expect(result.current.events).not.toContainEqual(eventToDelete); +}); + +it("이벤트 로딩 실패 시 '이벤트 로딩 실패'라는 텍스트와 함께 에러 토스트가 표시되어야 한다", async () => { + setupMockHandlerCreationFailure(); + + const { result } = renderHook(() => useEventOperations(false)); + + await act(async () => { + await result.current.fetchEvents(); + }); + + expect(enqueueSnackbarFn).toHaveBeenCalledWith('이벤트 로딩 실패', { variant: 'error' }); +}); + +it("존재하지 않는 이벤트 수정 시 '일정 저장 실패'라는 토스트가 노출되며 에러 처리가 되어야 한다", async () => { + setupMockHandlerUpdating(); + + const { result } = renderHook(() => useEventOperations(true)); + + await act(async () => { + await result.current.fetchEvents(); + }); + + const invalidEventToUpdate = { + id: '999', + title: '이벤트 2 new', + date: '2025-07-03', + startTime: '15:00', + endTime: '16:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + } as Event; -it("네트워크 오류 시 '일정 삭제 실패'라는 텍스트가 노출되며 이벤트 삭제가 실패해야 한다", async () => {}); + await act(async () => { + await result.current.saveEvent(invalidEventToUpdate); + }); + + expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정 저장 실패', { variant: 'error' }); +}); + +it("네트워크 오류 시 '일정 삭제 실패'라는 텍스트가 노출되며 이벤트 삭제가 실패해야 한다", async () => { + setupMockHandlerDeletionFailure(); + + const { result } = renderHook(() => useEventOperations(false)); + + await act(async () => { + await result.current.fetchEvents(); + }); + + const eventToDelete = result.current.events[0]; + + await act(async () => { + await result.current.deleteEvent(eventToDelete.id); + }); + + expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정 삭제 실패', { variant: 'error' }); +}); diff --git a/src/__tests__/hooks/medium.useNotifications.spec.ts b/src/__tests__/hooks/medium.useNotifications.spec.ts index 7f585ea8..8cfa0916 100644 --- a/src/__tests__/hooks/medium.useNotifications.spec.ts +++ b/src/__tests__/hooks/medium.useNotifications.spec.ts @@ -5,10 +5,92 @@ import { Event } from '../../types.ts'; import { formatDate } from '../../utils/dateUtils.ts'; import { parseHM } from '../utils.ts'; -it('초기 상태에서는 알림이 없어야 한다', () => {}); +const events = [ + { + id: '1', + title: '이벤트 1', + date: '2025-10-01', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 10, + }, + { + id: '2', + title: '이벤트 2', + date: '2025-10-02', + startTime: '14:00', + endTime: '15:00', + description: '동료와 점심 식사', + location: '회사 근처 식당', + category: '개인', + repeat: { type: 'none', interval: 0 }, + notificationTime: 10, + }, +] as Event[]; -it('지정된 시간이 된 경우 알림이 새롭게 생성되어 추가된다', () => {}); +it('초기 상태에서는 알림이 없어야 한다', () => { + const { result } = renderHook(() => useNotifications(events)); -it('index를 기준으로 알림을 적절하게 제거할 수 있다', () => {}); + expect(result.current.notifications).toHaveLength(0); + expect(result.current.notifiedEvents).toHaveLength(0); +}); -it('이미 알림이 발생한 이벤트에 대해서는 중복 알림이 발생하지 않아야 한다', () => {}); +it('지정된 시간이 된 경우 알림이 새롭게 생성되어 추가된다', () => { + const now = new Date('2025-10-01T13:50:00'); // 이벤트 시작 10분 전 + vi.setSystemTime(now); + + const { result } = renderHook(() => useNotifications(events)); + + act(() => { + vi.advanceTimersByTime(1000); + }); + + expect(result.current.notifications).toEqual([ + { id: '1', message: '10분 후 이벤트 1 일정이 시작됩니다.' }, + ]); +}); + +it('index를 기준으로 알림을 적절하게 제거할 수 있다', () => { + const now = new Date('2025-10-01T13:50:00'); // 이벤트 시작 10분 전 + vi.setSystemTime(now); + + const { result } = renderHook(() => useNotifications(events)); + + act(() => { + vi.advanceTimersByTime(1000); + }); + + expect(result.current.notifications).toHaveLength(1); + + act(() => { + result.current.removeNotification(0); + }); + + expect(result.current.notifications).toHaveLength(0); +}); + +it('이미 알림이 발생한 이벤트에 대해서는 중복 알림이 발생하지 않아야 한다', () => { + // 이벤트1의 알림 발생 시점(2025-10-01T13:50:00)으로 시간 설정 + const now = new Date('2025-10-01T13:50:00'); + vi.setSystemTime(now); + + const { result } = renderHook(() => useNotifications(events)); + + // 첫 번째 알림 발생 + act(() => { + vi.advanceTimersByTime(1000); + }); + + expect(result.current.notifications).toHaveLength(1); + + // 타이머를 여러 번 더 진행해도 중복 알림이 추가되지 않아야 함 + act(() => { + vi.advanceTimersByTime(1000 * 60 * 10); + }); + + expect(result.current.notifications).toHaveLength(1); +}); From 1037e72097380fa39862e116587e414ae8b07883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=92?= =?UTF-8?q?=E1=85=AE=E1=86=AB?= Date: Fri, 22 Aug 2025 16:07:11 +0900 Subject: [PATCH 5/7] =?UTF-8?q?Test:=20=ED=86=B5=ED=95=A9=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__mocks__/handlersUtils.ts | 7 +- src/__mocks__/response/realEvents.json | 65 +----- src/__tests__/medium.integration.spec.tsx | 251 ++++++++++++++++++++-- 3 files changed, 242 insertions(+), 81 deletions(-) diff --git a/src/__mocks__/handlersUtils.ts b/src/__mocks__/handlersUtils.ts index 15a1f2c8..aa66b582 100644 --- a/src/__mocks__/handlersUtils.ts +++ b/src/__mocks__/handlersUtils.ts @@ -99,6 +99,9 @@ export const setupMockHandlerDeletion = () => { export const setupMockHandlerCreationFailure = () => { server.use( + http.get('/api/events', () => { + return HttpResponse.json(null, { status: 500 }); + }), http.post('/api/events', async () => { return HttpResponse.json(null, { status: 500 }); }) @@ -125,8 +128,8 @@ export const setupMockHandlerDeletionFailure = () => { http.get('/api/events', () => { return HttpResponse.json({ events: mockEvents }); }), - http.delete('/api/events/:id', ({ params }) => { - return new HttpResponse({ error: 'Network error' }, { status: 500 }); + http.delete('/api/events/:id', () => { + return new HttpResponse(null, { status: 500 }); }) ); }; diff --git a/src/__mocks__/response/realEvents.json b/src/__mocks__/response/realEvents.json index 5ab618a0..f195be5f 100644 --- a/src/__mocks__/response/realEvents.json +++ b/src/__mocks__/response/realEvents.json @@ -1,64 +1 @@ -{ - "events": [ - { - "id": "2b7545a6-ebee-426c-b906-2329bc8d62bd", - "title": "팀 회의", - "date": "2025-08-20", - "startTime": "10:00", - "endTime": "11:00", - "description": "주간 팀 미팅", - "location": "회의실 A", - "category": "업무", - "repeat": { "type": "none", "interval": 0 }, - "notificationTime": 1 - }, - { - "id": "09702fb3-a478-40b3-905e-9ab3c8849dcd", - "title": "점심 약속", - "date": "2025-08-21", - "startTime": "12:30", - "endTime": "13:30", - "description": "동료와 점심 식사", - "location": "회사 근처 식당", - "category": "개인", - "repeat": { "type": "none", "interval": 0 }, - "notificationTime": 1 - }, - { - "id": "da3ca408-836a-4d98-b67a-ca389d07552b", - "title": "프로젝트 마감", - "date": "2025-08-25", - "startTime": "09:00", - "endTime": "18:00", - "description": "분기별 프로젝트 마감", - "location": "사무실", - "category": "업무", - "repeat": { "type": "none", "interval": 0 }, - "notificationTime": 1 - }, - { - "id": "dac62941-69e5-4ec0-98cc-24c2a79a7f81", - "title": "생일 파티", - "date": "2025-08-28", - "startTime": "19:00", - "endTime": "22:00", - "description": "친구 생일 축하", - "location": "친구 집", - "category": "개인", - "repeat": { "type": "none", "interval": 0 }, - "notificationTime": 1 - }, - { - "id": "80d85368-b4a4-47b3-b959-25171d49371f", - "title": "운동", - "date": "2025-08-22", - "startTime": "18:00", - "endTime": "19:00", - "description": "주간 운동", - "location": "헬스장", - "category": "개인", - "repeat": { "type": "none", "interval": 0 }, - "notificationTime": 1 - } - ] -} +{"events":[{"id":"2b7545a6-ebee-426c-b906-2329bc8d62bd","title":"팀 회의","date":"2025-08-20","startTime":"10:00","endTime":"11:00","description":"주간 팀 미팅","location":"회의실 A","category":"업무","repeat":{"type":"none","interval":0},"notificationTime":1},{"id":"09702fb3-a478-40b3-905e-9ab3c8849dcd","title":"점심 약속","date":"2025-08-21","startTime":"12:30","endTime":"13:30","description":"동료와 점심 식사","location":"회사 근처 식당","category":"개인","repeat":{"type":"none","interval":0},"notificationTime":1},{"id":"da3ca408-836a-4d98-b67a-ca389d07552b","title":"프로젝트 마감","date":"2025-08-25","startTime":"09:00","endTime":"18:00","description":"분기별 프로젝트 마감","location":"사무실","category":"업무","repeat":{"type":"none","interval":0},"notificationTime":1},{"id":"dac62941-69e5-4ec0-98cc-24c2a79a7f81","title":"생일 파티","date":"2025-08-28","startTime":"19:00","endTime":"22:00","description":"친구 생일 축하","location":"친구 집","category":"개인","repeat":{"type":"none","interval":0},"notificationTime":1},{"id":"80d85368-b4a4-47b3-b959-25171d49371f","title":"운동","date":"2025-08-22","startTime":"18:00","endTime":"19:00","description":"주간 운동","location":"헬스장","category":"개인","repeat":{"type":"none","interval":0},"notificationTime":1},{"id":"b567e5b4-24dc-42e1-b644-9be03a4d738c","title":"123","date":"2025-08-20","startTime":"10:00","endTime":"11:00","description":"123","location":"123","category":"개인","repeat":{"type":"none","interval":1},"notificationTime":10}]} \ No newline at end of file diff --git a/src/__tests__/medium.integration.spec.tsx b/src/__tests__/medium.integration.spec.tsx index 0b559b44..65044f73 100644 --- a/src/__tests__/medium.integration.spec.tsx +++ b/src/__tests__/medium.integration.spec.tsx @@ -1,6 +1,6 @@ import CssBaseline from '@mui/material/CssBaseline'; import { ThemeProvider, createTheme } from '@mui/material/styles'; -import { render, screen, within, act } from '@testing-library/react'; +import { render, screen, within, act, waitFor } from '@testing-library/react'; import { UserEvent, userEvent } from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; import { SnackbarProvider } from 'notistack'; @@ -58,38 +58,259 @@ const saveSchedule = async ( // ! HINT. "검색 결과가 없습니다"는 초기에 노출되는데요. 그럼 검증하고자 하는 액션이 실행되기 전에 검증해버리지 않을까요? 이 테스트를 신뢰성있게 만드려면 어떻게 할까요? describe('일정 CRUD 및 기본 기능', () => { it('입력한 새로운 일정 정보에 맞춰 모든 필드가 이벤트 리스트에 정확히 저장된다.', async () => { - // ! HINT. event를 추가 제거하고 저장하는 로직을 잘 살펴보고, 만약 그대로 구현한다면 어떤 문제가 있을 지 고민해보세요. + setupMockHandlerCreation(); + + const { user } = setup(); + + await saveSchedule(user, { + title: '테스트 일정', + date: '2025-10-01', + startTime: '10:00', + endTime: '11:00', + description: '새로운 일정 설명', + location: '새로운 일정 위치', + category: '개인', + }); + + const eventList = within(screen.getByTestId('event-list')); + expect(eventList.getByText('테스트 일정')).toBeInTheDocument(); + }); + + it('기존 일정의 세부 정보를 수정하고 변경사항이 정확히 반영된다', async () => { + setupMockHandlerUpdating(); + + const { user } = setup(); + + const editButton = await screen.findAllByLabelText('Edit event'); + + await user.click(editButton[0]); + + await user.clear(screen.getByLabelText('제목')); + await user.type(screen.getByLabelText('제목'), '수정된 테스트 일정'); + + await user.click(screen.getByTestId('event-submit-button')); + + const eventList = within(screen.getByTestId('event-list')); + expect(eventList.getByText('수정된 테스트 일정')).toBeInTheDocument(); }); - it('기존 일정의 세부 정보를 수정하고 변경사항이 정확히 반영된다', async () => {}); + it('일정을 삭제하고 더 이상 조회되지 않는지 확인한다', async () => { + setupMockHandlerDeletion(); + + const { user } = setup(); + const eventList = within(screen.getByTestId('event-list')); + expect(await eventList.findByText('삭제할 이벤트')).toBeInTheDocument(); - it('일정을 삭제하고 더 이상 조회되지 않는지 확인한다', async () => {}); + // 삭제 버튼 클릭 + const allDeleteButton = await screen.findAllByLabelText('Delete event'); + await user.click(allDeleteButton[0]); + + expect(eventList.queryByText('삭제할 이벤트')).not.toBeInTheDocument(); + }); }); describe('일정 뷰', () => { - it('주별 뷰를 선택 후 해당 주에 일정이 없으면, 일정이 표시되지 않는다.', async () => {}); + it('주별 뷰를 선택 후 해당 주에 일정이 없으면, 일정이 표시되지 않는다.', async () => { + const { user } = setup(); + + await user.click(within(screen.getByLabelText('뷰 타입 선택')).getByRole('combobox')); + await user.click(screen.getByRole('option', { name: 'week-option' })); + + const eventList = within(screen.getByTestId('event-list')); + expect(eventList.getByText('검색 결과가 없습니다.')).toBeInTheDocument(); + }); + + it('주별 뷰 선택 후 해당 일자에 일정이 존재한다면 해당 일정이 정확히 표시된다', async () => { + setupMockHandlerCreation(); + + const { user } = setup(); + + await saveSchedule(user, { + title: '주별 테스트 일정', + date: '2025-10-01', + startTime: '10:00', + endTime: '11:00', + description: '주별 테스트 일정 설명', + location: '주별 테스트 위치', + category: '개인', + }); + + await user.click(within(screen.getByLabelText('뷰 타입 선택')).getByRole('combobox')); + await user.click(screen.getByRole('option', { name: 'week-option' })); + + const eventList = within(screen.getByTestId('event-list')); + expect(eventList.getByText('주별 테스트 일정')).toBeInTheDocument(); + }); + + it('월별 뷰에 일정이 없으면, 일정이 표시되지 않아야 한다.', async () => { + setupMockHandlerCreation(); - it('주별 뷰 선택 후 해당 일자에 일정이 존재한다면 해당 일정이 정확히 표시된다', async () => {}); + const { user } = setup(); + + const eventList = within(screen.getByTestId('event-list')); + expect(eventList.getByText('검색 결과가 없습니다.')).toBeInTheDocument(); + }); + + it('월별 뷰에 일정이 정확히 표시되는지 확인한다', async () => { + setupMockHandlerCreation(); + + const { user } = setup(); + + await saveSchedule(user, { + title: '월별 테스트 일정', + date: '2025-10-01', + startTime: '10:00', + endTime: '11:00', + description: '월별 테스트 일정 설명', + location: '월별 테스트 위치', + category: '개인', + }); + + await user.click(within(screen.getByLabelText('뷰 타입 선택')).getByRole('combobox')); + await user.click(screen.getByRole('option', { name: 'month-option' })); + + const eventList = within(screen.getByTestId('event-list')); + expect(eventList.getByText('월별 테스트 일정')).toBeInTheDocument(); + }); - it('월별 뷰에 일정이 없으면, 일정이 표시되지 않아야 한다.', async () => {}); + it('달력에 1월 1일(신정)이 공휴일로 표시되는지 확인한다', async () => { + vi.setSystemTime(new Date('2025-01-01')); - it('월별 뷰에 일정이 정확히 표시되는지 확인한다', async () => {}); + setup(); - it('달력에 1월 1일(신정)이 공휴일로 표시되는지 확인한다', async () => {}); + const calendar = screen.getByTestId('month-view'); + + expect(within(calendar).getByText('신정')).toBeInTheDocument(); + }); }); describe('검색 기능', () => { - it('검색 결과가 없으면, "검색 결과가 없습니다."가 표시되어야 한다.', async () => {}); + beforeEach(() => { + setupMockHandlerCreation([ + { + id: '2b7545a6-ebee-426c-b906-2329bc8d62bd', + title: '팀 회의', + date: '2025-10-01', + startTime: '10:00', + endTime: '11:00', + description: '주간 팀 미팅', + location: '회의실 A', + category: '업무', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + ]); + }); + + it('검색 결과가 없으면, "검색 결과가 없습니다."가 표시되어야 한다.', async () => { + const { user } = setup(); + + const searchInput = screen.getByPlaceholderText('검색어를 입력하세요'); + await user.type(searchInput, '없는 일정'); + + const eventList = within(screen.getByTestId('event-list')); + expect(eventList.getByText('검색 결과가 없습니다.')).toBeInTheDocument(); + }); + + it("'팀 회의'를 검색하면 해당 제목을 가진 일정이 리스트에 노출된다", async () => { + const { user } = setup(); - it("'팀 회의'를 검색하면 해당 제목을 가진 일정이 리스트에 노출된다", async () => {}); + const searchInput = screen.getByPlaceholderText('검색어를 입력하세요'); + await user.type(searchInput, '팀 회의'); - it('검색어를 지우면 모든 일정이 다시 표시되어야 한다', async () => {}); + const eventList = within(screen.getByTestId('event-list')); + expect(eventList.getByText('팀 회의')).toBeInTheDocument(); + }); + + it('검색어를 지우면 모든 일정이 다시 표시되어야 한다', async () => { + const { user } = setup(); + + const searchInput = screen.getByPlaceholderText('검색어를 입력하세요'); + await user.clear(searchInput); + + const eventList = within(screen.getByTestId('event-list')); + expect(eventList.getByText('팀 회의')).toBeInTheDocument(); + }); }); describe('일정 충돌', () => { - it('겹치는 시간에 새 일정을 추가할 때 경고가 표시된다', async () => {}); + beforeEach(() => { + setupMockHandlerCreation([ + { + id: '2b7545a6-ebee-426c-b906-2329bc8d62bd', + title: '팀 회의', + date: '2025-10-01', + startTime: '10:00', + endTime: '11:00', + description: '주간 팀 미팅', + location: '회의실 A', + category: '업무', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + { + id: '2b7545a6-ebee-426c-b906-2329bc8d62bd', + title: '팀 회의2', + date: '2025-10-01', + startTime: '11:00', + endTime: '12:00', + description: '주간 팀 미팅', + location: '회의실 A', + category: '업무', + repeat: { type: 'none', interval: 0 }, + notificationTime: 1, + }, + ]); + }); + + it('겹치는 시간에 새 일정을 추가할 때 경고가 표시된다', async () => { + const { user } = setup(); + + await saveSchedule(user, { + title: '새 회의', + date: '2025-10-01', + startTime: '10:00', + endTime: '11:00', + description: '설명', + location: '회의실 A', + category: '업무', + }); - it('기존 일정의 시간을 수정하여 충돌이 발생하면 경고가 노출된다', async () => {}); + expect(screen.getByText('일정 겹침 경고')).toBeInTheDocument(); + }); + + it('기존 일정의 시간을 수정하여 충돌이 발생하면 경고가 노출된다', async () => { + setupMockHandlerUpdating(); + + const { user } = setup(); + + const editButton = (await screen.findAllByLabelText('Edit event'))[1]; + await user.click(editButton); + + // 시간 수정하여 다른 일정과 충돌 발생 + await user.clear(screen.getByLabelText('시작 시간')); + await user.type(screen.getByLabelText('시작 시간'), '09:00'); + await user.clear(screen.getByLabelText('종료 시간')); + await user.type(screen.getByLabelText('종료 시간'), '10:00'); + + await user.click(screen.getByTestId('event-submit-button')); + + expect(screen.getByText('일정 겹침 경고')).toBeInTheDocument(); + }); }); -it('notificationTime을 10으로 하면 지정 시간 10분 전 알람 텍스트가 노출된다', async () => {}); +it('notificationTime을 10으로 하면 지정 시간 10분 전 알람 텍스트가 노출된다', async () => { + vi.setSystemTime(new Date('2025-10-15 08:49:59')); + + setup(); + + await screen.findByText('일정 로딩 완료!'); + + expect(screen.queryByText('10분 후 기존 회의 일정이 시작됩니다.')).not.toBeInTheDocument(); + + act(() => { + vi.advanceTimersByTime(1000); + }); + + expect(screen.getByText('10분 후 기존 회의 일정이 시작됩니다.')).toBeInTheDocument(); +}); From 2643e8686c6f5692999d3b4f54a065d23afe2999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=92?= =?UTF-8?q?=E1=85=AE=E1=86=AB?= Date: Fri, 22 Aug 2025 16:10:45 +0900 Subject: [PATCH 6/7] =?UTF-8?q?Style:=20lint=20error=20=EC=88=98=EC=A0=95.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/hooks/easy.useCalendarView.spec.ts | 1 - src/__tests__/hooks/medium.useNotifications.spec.ts | 2 -- src/__tests__/medium.integration.spec.tsx | 6 ++---- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/__tests__/hooks/easy.useCalendarView.spec.ts b/src/__tests__/hooks/easy.useCalendarView.spec.ts index 3f9ea872..8a751e75 100644 --- a/src/__tests__/hooks/easy.useCalendarView.spec.ts +++ b/src/__tests__/hooks/easy.useCalendarView.spec.ts @@ -1,7 +1,6 @@ import { act, renderHook } from '@testing-library/react'; import { useCalendarView } from '../../hooks/useCalendarView.ts'; -import { assertDate } from '../utils.ts'; describe('초기 상태', () => { it('view는 "month"이어야 한다', () => { diff --git a/src/__tests__/hooks/medium.useNotifications.spec.ts b/src/__tests__/hooks/medium.useNotifications.spec.ts index 8cfa0916..e75d803f 100644 --- a/src/__tests__/hooks/medium.useNotifications.spec.ts +++ b/src/__tests__/hooks/medium.useNotifications.spec.ts @@ -2,8 +2,6 @@ import { act, renderHook } from '@testing-library/react'; import { useNotifications } from '../../hooks/useNotifications.ts'; import { Event } from '../../types.ts'; -import { formatDate } from '../../utils/dateUtils.ts'; -import { parseHM } from '../utils.ts'; const events = [ { diff --git a/src/__tests__/medium.integration.spec.tsx b/src/__tests__/medium.integration.spec.tsx index 65044f73..ae3e7d73 100644 --- a/src/__tests__/medium.integration.spec.tsx +++ b/src/__tests__/medium.integration.spec.tsx @@ -1,8 +1,7 @@ import CssBaseline from '@mui/material/CssBaseline'; import { ThemeProvider, createTheme } from '@mui/material/styles'; -import { render, screen, within, act, waitFor } from '@testing-library/react'; +import { render, screen, within, act } from '@testing-library/react'; import { UserEvent, userEvent } from '@testing-library/user-event'; -import { http, HttpResponse } from 'msw'; import { SnackbarProvider } from 'notistack'; import { ReactElement } from 'react'; @@ -12,7 +11,6 @@ import { setupMockHandlerUpdating, } from '../__mocks__/handlersUtils'; import App from '../App'; -import { server } from '../setupTests'; import { Event } from '../types'; const theme = createTheme(); @@ -145,7 +143,7 @@ describe('일정 뷰', () => { it('월별 뷰에 일정이 없으면, 일정이 표시되지 않아야 한다.', async () => { setupMockHandlerCreation(); - const { user } = setup(); + setup(); const eventList = within(screen.getByTestId('event-list')); expect(eventList.getByText('검색 결과가 없습니다.')).toBeInTheDocument(); From 27013976d5cd579bd11fdfa2447a3e4c1f6e13f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=92?= =?UTF-8?q?=E1=85=AE=E1=86=AB?= Date: Mon, 8 Sep 2025 14:03:02 +0900 Subject: [PATCH 7/7] Docs: docs. --- docs/devLog.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/devLog.md diff --git a/docs/devLog.md b/docs/devLog.md new file mode 100644 index 00000000..f0d32d93 --- /dev/null +++ b/docs/devLog.md @@ -0,0 +1,35 @@ +## easy.useSearch.spec + +1. new Date() 시스템 시간 값이 이상하다 + 지금 8월인데 10월로 나온다 + 이것때문에 dummy event 8월로 만들어두고 하루종일 테스트 통과 안되다가 + 설마하고 new Date() 콘솔에 찍어보니까 10월이라 열받는다. + + ```ts + beforeEach(() => { + expect.hasAssertions(); // ? Med: 이걸 왜 써야하는지 물어보자 + + vi.setSystemTime(new Date('2025-10-01')); // ? Med: 이걸 왜 써야하는지 물어보자 + }); + ``` + + 이것 때문이네!! + +## easy.dateUtils.spec + +1. getDaysInMonth + + ```ts + function getDaysInMonth(year: number, month: number): number { + return new Date(year, month, 0).getDate(); + } + ``` + + day에 0을 넣음으로써 monthIndex에 넣은 정수의 -1월을 계산한다. + + > new Date(2025, 8, 0) -> 여기서 `8`은 `9월`, + > day가 `0`으로 `9월`에서 `-1`을 한 `8월`로 계산됨. + > month param에 `8`을 넣고 결과값도 `8월`로 계산됨. + + monthIndex는 0~11로 월을 판단하는데, 우리가 쓰는 1~12월과는 -1씩 차이나기 때문에 moth parameter가 우리가 실사용하는 월로 계산될 수 있다. + 함수 제대로 안 보고 막 작성했다가 꽤나 꼬였었음.