From 29f097ba4908da17f8f465bdbfd6dabe8f588d91 Mon Sep 17 00:00:00 2001 From: six-standard Date: Sat, 12 Jul 2025 19:18:13 +0900 Subject: [PATCH 01/14] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__test__/instance.test.tsx | 6 --- src/__test__/login.test.tsx | 77 ---------------------------------- src/__test__/main.test.tsx | 76 --------------------------------- 3 files changed, 159 deletions(-) delete mode 100644 src/__test__/instance.test.tsx delete mode 100644 src/__test__/login.test.tsx delete mode 100644 src/__test__/main.test.tsx diff --git a/src/__test__/instance.test.tsx b/src/__test__/instance.test.tsx deleted file mode 100644 index 6972106..0000000 --- a/src/__test__/instance.test.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { render, RenderResult } from '@testing-library/react'; -import { ReactElement } from 'react'; -import { QueryProvider } from '@/app/components/Provider/QueryProvider'; - -export const renderWithQueryClient = (element: ReactElement): RenderResult => - render({element}); diff --git a/src/__test__/login.test.tsx b/src/__test__/login.test.tsx deleted file mode 100644 index d45aacb..0000000 --- a/src/__test__/login.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { act, screen } from '@testing-library/react'; -import { userEvent } from '@testing-library/user-event'; -import { useRouter } from 'next/navigation'; -import { ToastContainer } from 'react-toastify'; -import { default as Login } from '@/app/(login)/page'; -import { renderWithQueryClient } from './instance.test'; - -jest.mock('next/navigation', () => ({ - useRouter: jest.fn(), -})); - -const getElements = () => { - const buttonEl = screen.getByRole('button'); - const accessInputEl = screen.getByPlaceholderText('Access Token을 입력하세요'); - const refreshInputEl = screen.getByPlaceholderText('Refresh Token을 입력하세요'); - - return { buttonEl, accessInputEl, refreshInputEl }; -}; - -const renderPage = () => { - renderWithQueryClient( - <> - - - , - ); -}; - -describe('로그인 화면에서', () => { - beforeEach(() => { - (useRouter as jest.Mock).mockImplementation(() => ({})); - }); - - it('입력 칸이 하나라도 비어있으면 버튼이 비활성화된다.', async () => { - renderPage(); - const { buttonEl, accessInputEl } = getElements(); - await userEvent.type(accessInputEl, 'access'); - expect(buttonEl).toBeDisabled(); - }); - - it('액세스 토큰과 리프레시 토큰을 입력하면 버튼이 활성화된다', async () => { - renderPage(); - const { buttonEl, accessInputEl, refreshInputEl } = getElements(); - await userEvent.type(accessInputEl, 'access'); - await userEvent.type(refreshInputEl, 'refresh'); - expect(buttonEl).toBeEnabled(); - }); - - describe('API 요청에서', () => { - it('액세스 토큰이 비정상적이면 오류 토스트가 표기된다', async () => { - renderPage(); - const { buttonEl, accessInputEl, refreshInputEl } = getElements(); - - await userEvent.type(accessInputEl, 'invalid_access'); - await userEvent.type(refreshInputEl, 'invalid_refresh'); - await act(async () => buttonEl.click()); - - const toastEl = screen.getByText('일치하는 계정을 찾을 수 없습니다'); - expect(toastEl).not.toBeUndefined(); - }); - - it('요청이 성공하면 페이지를 대시보드로 이동시킨다', async () => { - renderPage(); - - const replace = jest.fn(); - (useRouter as jest.Mock).mockImplementation(() => ({ replace })); - - const { buttonEl, accessInputEl, refreshInputEl } = getElements(); - - await userEvent.type(accessInputEl, 'access'); - await userEvent.type(refreshInputEl, 'refresh'); - await act(async () => buttonEl.click()); - - expect(replace).toHaveBeenCalledWith('/main?asc=false&sort='); - }); - }); -}); diff --git a/src/__test__/main.test.tsx b/src/__test__/main.test.tsx deleted file mode 100644 index cc3c2d1..0000000 --- a/src/__test__/main.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { screen, waitFor } from '@testing-library/react'; -import { Content } from '@/app/(auth-required)/main/Content'; -import { Header } from '@/app/components/Header'; -import { renderWithQueryClient } from './instance.test'; - -jest.mock('next/navigation', () => ({ - useSearchParams: () => ({ - searchParams: [{ asc: 'false', sort: '' }], - }), - useRouter: () => ({ - replace: jest.fn(), - }), - usePathname: () => 'http://localhost:3000', -})); - -jest.mock(`react-intersection-observer`, () => ({ - useInView: () => ({ ref: () => {}, inView: true }), -})); - -const withFetch = (withOriginalFetch?: typeof global.fetch) => { - if (withOriginalFetch) { - global.fetch = withOriginalFetch; - return; - } - - const fetchApi = global.fetch; - global.fetch = async (input, init) => { - return await fetchApi(input, { - ...init, - headers: { - ...Object.fromEntries(Array.from(init?.headers as Headers)), - access_token: 'access', - refresh_token: 'refresh', - }, - }); - }; -}; - -const originalFetch = () => global.fetch; // header가 없을 경우의 fetch - -describe('메인(대시보드) 페이지에서', () => { - describe('API에서', () => { - it('401/403 오류가 발생하면 로그인 페이지로 이동한다.', async () => { - withFetch(originalFetch()); - renderWithQueryClient(); - const replace = jest.fn(); - - const location = new URL('http://localhost:3000'); - (location as unknown as Location).replace = replace; - - delete (window as unknown as Partial).location; - window.location = location as unknown as Location; - - await waitFor(() => expect(replace).toHaveBeenCalledWith('/')); - }); - - it('프로필 API 요청이 성공하면 정상적으로 데이터를 표시한다', async () => { - withFetch(); - renderWithQueryClient(
); - - const profile = await screen.findByText('test'); - - expect(profile).not.toBeUndefined(); - }); - - it('게시글 정보 요약 API 요청이 성공하면 정상적으로 데이터를 표시한다', async () => { - withFetch(); - const { container } = renderWithQueryClient(); - - await waitFor(() => - // eslint-disable-next-line - expect(container.querySelector('span#totalViews')?.innerHTML).toBe('100회'), - ); - }); - }); -}); From 6963acb764c33bcf949b1e8a004c8b16833f085e Mon Sep 17 00:00:00 2001 From: six-standard Date: Sat, 12 Jul 2025 19:18:38 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EC=9C=A0=ED=8B=B8=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/__tests__/datetime.util.test.ts | 91 +++++++++++++++++++++++ src/utils/__tests__/number.util.test.ts | 52 +++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/utils/__tests__/datetime.util.test.ts create mode 100644 src/utils/__tests__/number.util.test.ts diff --git a/src/utils/__tests__/datetime.util.test.ts b/src/utils/__tests__/datetime.util.test.ts new file mode 100644 index 0000000..9b66e66 --- /dev/null +++ b/src/utils/__tests__/datetime.util.test.ts @@ -0,0 +1,91 @@ +import { convertDateToKST, formatTimeToMMSS, KSTDateFormat } from '../datetime.util'; + +describe('datetime.util', () => { + describe('convertDateToKST', () => { + it('UTC 날짜를 KST로 정확히 변환해야 한다', () => { + const utcDate = '2025-01-01T00:00:00.000Z'; + const result = convertDateToKST(utcDate); + + expect(result).toBeDefined(); + expect(result?.short).toBe('2025-01-01'); + expect(result?.iso).toBe('2025-01-01T09:00:00+09:00'); + expect(result?.full).toBeInstanceOf(Date); + }); + + it('다른 시간대의 UTC 날짜도 정확히 변환해야 한다', () => { + const utcDate = '2024-12-31T15:30:45.123Z'; + const result = convertDateToKST(utcDate); + + expect(result).toBeDefined(); + expect(result?.short).toBe('2025-01-01'); + expect(result?.iso).toBe('2025-01-01T00:30:45+09:00'); + }); + + it('undefined 또는 빈 문자열이 전달되면 undefined를 반환해야 한다', () => { + expect(convertDateToKST(undefined)).toBeUndefined(); + expect(convertDateToKST('')).toBeUndefined(); + }); + + it('잘못된 날짜 형식이 전달되면 Invalid Date를 처리해야 한다', () => { + const invalidDate = 'invalid-date'; + const result = convertDateToKST(invalidDate); + + expect(result).toBeDefined(); + expect(result?.full.toString()).toBe('Invalid Date'); + }); + + it('날짜 경계값을 올바르게 처리해야 한다', () => { + const utcDate = '2024-12-31T23:59:59.000Z'; + const result = convertDateToKST(utcDate); + + expect(result).toBeDefined(); + expect(result?.short).toBe('2025-01-01'); + expect(result?.iso).toBe('2025-01-01T08:59:59+09:00'); + }); + + it('반환된 객체가 올바른 구조를 가져야 한다', () => { + const utcDate = '2025-01-01T00:00:00.000Z'; + const result = convertDateToKST(utcDate) as KSTDateFormat; + + expect(result).toHaveProperty('short'); + expect(result).toHaveProperty('iso'); + expect(result).toHaveProperty('full'); + expect(typeof result.short).toBe('string'); + expect(typeof result.iso).toBe('string'); + expect(result.full).toBeInstanceOf(Date); + }); + }); + + describe('formatTimeToMMSS', () => { + it('정확한 분과 초로 변환해야 한다', () => { + expect(formatTimeToMMSS(0)).toBe('00분 00초'); + expect(formatTimeToMMSS(59)).toBe('00분 59초'); + expect(formatTimeToMMSS(60)).toBe('01분 00초'); + expect(formatTimeToMMSS(61)).toBe('01분 01초'); + expect(formatTimeToMMSS(120)).toBe('02분 00초'); + expect(formatTimeToMMSS(125)).toBe('02분 05초'); + }); + + it('큰 숫자도 올바르게 변환해야 한다', () => { + expect(formatTimeToMMSS(3600)).toBe('60분 00초'); + expect(formatTimeToMMSS(3661)).toBe('61분 01초'); + }); + + it('소수점이 포함된 숫자를 처리해야 한다', () => { + expect(formatTimeToMMSS(65.7)).toBe('01분 05초'); + expect(formatTimeToMMSS(59.9)).toBe('00분 59초'); + }); + + it('경계값들을 정확히 처리해야 한다', () => { + expect(formatTimeToMMSS(59)).toBe('00분 59초'); + expect(formatTimeToMMSS(60)).toBe('01분 00초'); + expect(formatTimeToMMSS(119)).toBe('01분 59초'); + expect(formatTimeToMMSS(120)).toBe('02분 00초'); + }); + + it('한 자리 수는 0으로 패딩해야 한다', () => { + expect(formatTimeToMMSS(5)).toBe('00분 05초'); + expect(formatTimeToMMSS(65)).toBe('01분 05초'); + }); + }); +}); diff --git a/src/utils/__tests__/number.util.test.ts b/src/utils/__tests__/number.util.test.ts new file mode 100644 index 0000000..22b58a7 --- /dev/null +++ b/src/utils/__tests__/number.util.test.ts @@ -0,0 +1,52 @@ +import { parseNumber } from '../number.util'; + +describe('number.util', () => { + describe('parseNumber', () => { + it('정수에 천 단위 콤마를 추가해야 한다', () => { + expect(parseNumber(1000)).toBe('1,000'); + expect(parseNumber(1234)).toBe('1,234'); + expect(parseNumber(12345)).toBe('12,345'); + expect(parseNumber(123456)).toBe('123,456'); + expect(parseNumber(1234567)).toBe('1,234,567'); + }); + + it('천 단위 미만의 숫자는 콤마 없이 반환해야 한다', () => { + expect(parseNumber(0)).toBe('0'); + expect(parseNumber(1)).toBe('1'); + expect(parseNumber(99)).toBe('99'); + expect(parseNumber(999)).toBe('999'); + }); + + it('음수에도 콤마를 추가해야 한다', () => { + expect(parseNumber(-1000)).toBe('-1,000'); + expect(parseNumber(-1234567)).toBe('-1,234,567'); + }); + + it('undefined가 전달되면 "0"을 반환해야 한다', () => { + expect(parseNumber(undefined)).toBe('0'); + }); + + it('NaN과 Infinity를 처리해야 한다', () => { + expect(parseNumber(NaN)).toBe('0'); + expect(parseNumber(Infinity)).toBe('0'); + expect(parseNumber(-Infinity)).toBe('0'); + }); + + it('소수점이 포함된 숫자는 정수 부분만 처리해야 한다', () => { + expect(parseNumber(1234.56)).toBe('1,234'); + expect(parseNumber(999.99)).toBe('999'); + expect(parseNumber(0.9)).toBe('0'); + }); + + it('경계값들을 정확히 처리해야 한다', () => { + expect(parseNumber(999)).toBe('999'); + expect(parseNumber(1000)).toBe('1,000'); + expect(parseNumber(9999)).toBe('9,999'); + expect(parseNumber(10000)).toBe('10,000'); + }); + + it('매우 큰 숫자도 올바르게 처리해야 한다', () => { + expect(parseNumber(1234567890)).toBe('1,234,567,890'); + }); + }); +}); From 8c309872838d999920ba794c01b5955ed4f2e7f7 Mon Sep 17 00:00:00 2001 From: six-standard Date: Sat, 12 Jul 2025 19:18:53 +0900 Subject: [PATCH 03/14] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/datetime.util.ts | 4 ++-- src/utils/number.util.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/utils/datetime.util.ts b/src/utils/datetime.util.ts index 13a4f4e..b03c801 100644 --- a/src/utils/datetime.util.ts +++ b/src/utils/datetime.util.ts @@ -55,10 +55,10 @@ export const convertDateToKST = (date?: string): KSTDateFormat | undefined => { */ export const formatTimeToMMSS = (time: number) => { - const minute = Math.floor(time / 60) + const minute = Math.floor(Math.floor(time) / 60) .toString() .padStart(2, '0'); - const second = (time % 60).toString().padStart(2, '0'); + const second = (Math.floor(time) % 60).toString().padStart(2, '0'); return `${minute}분 ${second}초`; }; diff --git a/src/utils/number.util.ts b/src/utils/number.util.ts index 58452ed..811b228 100644 --- a/src/utils/number.util.ts +++ b/src/utils/number.util.ts @@ -5,5 +5,9 @@ * @returns {string} */ -export const parseNumber = (item?: number) => - item ? item.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : '0'; +export const parseNumber = (item?: number) => { + if (item === undefined || Math.abs(item) === Infinity || isNaN(item)) return '0'; + return Math.floor(item) + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ','); +}; From f380499444e37446b00478295589aea5da491365 Mon Sep 17 00:00:00 2001 From: six-standard Date: Sat, 12 Jul 2025 19:22:06 +0900 Subject: [PATCH 04/14] =?UTF-8?q?fix:=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__mock__/handlers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__mock__/handlers.ts b/src/__mock__/handlers.ts index cffe092..f0f6f17 100644 --- a/src/__mock__/handlers.ts +++ b/src/__mock__/handlers.ts @@ -1,9 +1,9 @@ import { http } from 'msw'; -import { env, PATHS } from '@/constants'; +import { ENVS, PATHS } from '@/constants'; import { LoginVo } from '@/types'; import { BaseError, BaseSuccess } from './responses'; -const BASE_URL = env.BASE_URL + '/api'; +const BASE_URL = ENVS.BASE_URL + '/api'; const login = http.post(`${BASE_URL}${PATHS.LOGIN}`, async ({ request }) => { const { accessToken, refreshToken } = (await request.json()) as LoginVo; From 3ea7cb89547f4716783d2efa88fedea3a101776b Mon Sep 17 00:00:00 2001 From: six-standard Date: Sun, 13 Jul 2025 07:54:26 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feature:=20Cypress=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=20E2E=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README_TESTING.md | 219 +++++++ cypress.config.ts | 26 + cypress/e2e/leaderboards.cy.ts | 118 ++++ cypress/e2e/login.cy.ts | 69 ++ cypress/e2e/main.cy.ts | 102 +++ cypress/support/commands.ts | 36 ++ cypress/support/e2e.ts | 376 +++++++++++ package.json | 17 +- pnpm-lock.yaml | 1091 +++++++++++++++++++++++++++++++- public/mockServiceWorker.js | 307 +++++++++ src/__mock__/browser.ts | 28 + src/__mock__/handlers.ts | 482 ++++++++++++-- src/__mock__/responses.ts | 57 +- src/__mock__/server.ts | 39 +- src/app/msw-provider.tsx | 34 + 15 files changed, 2896 insertions(+), 105 deletions(-) create mode 100644 README_TESTING.md create mode 100644 cypress.config.ts create mode 100644 cypress/e2e/leaderboards.cy.ts create mode 100644 cypress/e2e/login.cy.ts create mode 100644 cypress/e2e/main.cy.ts create mode 100644 cypress/support/commands.ts create mode 100644 cypress/support/e2e.ts create mode 100644 public/mockServiceWorker.js create mode 100644 src/__mock__/browser.ts create mode 100644 src/app/msw-provider.tsx diff --git a/README_TESTING.md b/README_TESTING.md new file mode 100644 index 0000000..635200f --- /dev/null +++ b/README_TESTING.md @@ -0,0 +1,219 @@ +# 테스트 설정 가이드 + +## 🚀 MSW + Cypress 테스트 환경 구성 + +이 프로젝트는 **MSW(Mock Service Worker)**와 **Cypress**를 활용한 E2E 테스트 환경을 구성하였습니다. + +### 📋 주요 특징 + +- **API 프로젝트 응답 형식에 맞춘 MSW 모킹** +- **로그인 건너뛰기 기능 (사전 인증 토큰 설정)** +- **모든 API 엔드포인트에 대한 완전한 모킹** +- **실제 네트워크 요청 없이 빠른 테스트 실행** + +### 🔧 설치된 패키지 + +```bash +# MSW 관련 +msw@^2.7.3 + +# Cypress 관련 +cypress@^14.5.1 +@cypress/webpack-preprocessor@^6.0.4 +start-server-and-test@^2.0.12 +``` + +### 🏗️ 프로젝트 구조 + +``` +WEB/ +├── src/ +│ └── __mock__/ +│ ├── handlers.ts # MSW 핸들러 (모든 API 엔드포인트) +│ ├── responses.ts # 응답 헬퍼 함수 +│ ├── server.ts # Node.js 환경용 MSW 서버 +│ └── browser.ts # 브라우저 환경용 MSW 워커 +├── cypress/ +│ ├── e2e/ +│ │ ├── login.cy.ts # 로그인 페이지 테스트 +│ │ ├── main.cy.ts # 메인 페이지 테스트 +│ │ └── leaderboards.cy.ts # 리더보드 페이지 테스트 +│ └── support/ +│ ├── e2e.ts # Cypress 지원 파일 +│ └── commands.ts # 커스텀 명령어 +├── cypress.config.ts # Cypress 설정 +└── jest.setup.ts # Jest + MSW 설정 +``` + +### 📝 MSW 핸들러 구성 + +모든 API 엔드포인트에 대한 MSW 핸들러가 구성되어 있습니다: + +#### 사용자 관련 API + +- `POST /api/login` - 사용자 로그인 +- `POST /api/login-sample` - 샘플 로그인 +- `POST /api/logout` - 로그아웃 +- `GET /api/me` - 현재 사용자 정보 +- `POST /api/qr-login` - QR 로그인 토큰 생성 +- `GET /api/qr-login` - QR 로그인 토큰 조회 + +#### 게시물 관련 API + +- `GET /api/posts` - 게시물 목록 조회 +- `GET /api/posts-stats` - 게시물 통계 +- `GET /api/post/:postId` - 게시물 상세 조회 (ID 기반) +- `GET /api/post/velog/:postId` - 게시물 상세 조회 (UUID 기반) + +#### 리더보드 관련 API + +- `GET /api/leaderboard/user` - 사용자 리더보드 +- `GET /api/leaderboard/post` - 게시물 리더보드 + +#### 기타 API + +- `GET /api/total-stats` - 전체 통계 +- `GET /api/notis` - 공지사항 +- `POST /api/webhook/sentry` - Sentry 웹훅 + +### 🎯 테스트 실행 방법 + +#### 1. Cypress 테스트 실행 + +```bash +# 개발 환경에서 Cypress 열기 +pnpm e2e:dev + +# 헤드리스 모드로 Cypress 실행 +pnpm e2e:test + +# Cypress만 실행 (서버가 이미 실행 중일 때) +pnpm cypress:open +pnpm cypress:run +``` + +#### 2. Jest 테스트 실행 + +```bash +# 일반 Jest 테스트 +pnpm test +``` + +### 🔐 인증 토큰 관리 + +#### 모킹 토큰 + +```typescript +// src/__mock__/handlers.ts +export const MOCK_ACCESS_TOKEN = 'mock_access_token_12345'; +export const MOCK_REFRESH_TOKEN = 'mock_refresh_token_67890'; +``` + +#### 로그인 건너뛰기 + +로그인 페이지를 제외한 모든 페이지 테스트에서는 `cy.setAuthCookies()` 명령어를 사용하여 사전에 인증 토큰을 설정합니다: + +```typescript +// cypress/e2e/main.cy.ts +describe('메인 페이지', () => { + beforeEach(() => { + // 인증 토큰 설정 (로그인 건너뛰기) + cy.setAuthCookies(); + + // 메인 페이지 방문 + cy.visit('/main'); + }); + + // ... 테스트 코드 +}); +``` + +### 🛠️ 커스텀 Cypress 명령어 + +#### `cy.setAuthCookies()` + +인증 토큰을 쿠키에 설정하여 로그인 상태를 모킹합니다. + +#### `cy.clearAuthCookies()` + +인증 토큰을 쿠키에서 제거합니다. + +#### `cy.waitForPageLoad()` + +페이지 로드가 완료될 때까지 대기합니다. + +### 📊 테스트 범위 + +#### 로그인 페이지 (`login.cy.ts`) + +- ✅ 페이지 로드 확인 +- ✅ 로그인 폼 존재 확인 +- ✅ 유효한 토큰으로 로그인 성공 +- ✅ 유효하지 않은 토큰으로 로그인 실패 +- ✅ 샘플 로그인 기능 + +#### 메인 페이지 (`main.cy.ts`) + +- ✅ 페이지 로드 확인 +- ✅ 대시보드 통계 정보 표시 +- ✅ 게시물 목록 표시 +- ✅ 사이드바 네비게이션 동작 +- ✅ 헤더 정보 표시 +- ✅ 차트 렌더링 +- ✅ 정렬 기능 동작 +- ✅ 페이지네이션 동작 +- ✅ 로그아웃 기능 + +#### 리더보드 페이지 (`leaderboards.cy.ts`) + +- ✅ 페이지 로드 확인 +- ✅ 사용자 리더보드 표시 +- ✅ 게시물 리더보드 표시 +- ✅ 필터 기능 동작 +- ✅ 랭킹 순위 표시 +- ✅ 통계 변화량 표시 +- ✅ 헤더 네비게이션 동작 +- ✅ 프로필 이미지 표시 + +### 🔄 개발 워크플로우 + +1. **개발 서버 실행** + + ```bash + pnpm dev + ``` + +2. **테스트 실행** + + ```bash + # 새 터미널에서 + pnpm e2e:dev + ``` + +3. **테스트 작성** + - `cypress/e2e/` 폴더에 `*.cy.ts` 파일 생성 + - 필요한 경우 `src/__mock__/handlers.ts`에 새로운 API 핸들러 추가 + +### 🚨 주의사항 + +1. **실제 API 호출 금지**: 모든 API 요청은 MSW를 통해 모킹됩니다. +2. **토큰 관리**: 로그인 테스트 외에는 반드시 `cy.setAuthCookies()`를 사용하세요. +3. **선택자 전략**: 가능한 경우 `data-testid` 속성을 사용하고, 없는 경우 텍스트 기반 선택자를 사용합니다. + +### 📈 성능 최적화 + +- MSW를 통한 네트워크 요청 모킹으로 빠른 테스트 실행 +- 로그인 건너뛰기를 통한 테스트 시간 단축 +- 병렬 테스트 실행 지원 + +### 🎉 완료된 설정 + +✅ 기존 MSW 코드 제거 +✅ API 프로젝트 응답 형식에 맞춘 새로운 MSW 구성 +✅ 모든 API 엔드포인트에 대한 MSW 핸들러 생성 +✅ Cypress 설정 및 기본 구성 +✅ 각 페이지별 Cypress 테스트 코드 생성 +✅ 로그인 건너뛰기 기능 구현 +✅ Jest + MSW 통합 설정 + +이제 안정적이고 빠른 E2E 테스트를 실행할 수 있습니다! 🚀 diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 0000000..c5b5b40 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:3000', + supportFile: 'cypress/support/e2e.ts', + specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', + viewportWidth: 1920, + viewportHeight: 1080, + video: false, + screenshotOnRunFailure: true, + defaultCommandTimeout: 10000, + requestTimeout: 10000, + responseTimeout: 10000, + env: { + NEXT_PUBLIC_BASE_URL: 'http://localhost:3000', + NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY: 'test_key', + NEXT_PUBLIC_GA_ID: 'test_ga_id', + NEXT_PUBLIC_SENTRY_AUTH_TOKEN: 'test_sentry_token', + NEXT_PUBLIC_SENTRY_DSN: 'test_sentry_dsn', + }, + setupNodeEvents(_on, _config) { + // implement node event listeners here + }, + }, +}); diff --git a/cypress/e2e/leaderboards.cy.ts b/cypress/e2e/leaderboards.cy.ts new file mode 100644 index 0000000..7ddbec0 --- /dev/null +++ b/cypress/e2e/leaderboards.cy.ts @@ -0,0 +1,118 @@ +// *********************************************************** +// 리더보드 페이지 E2E 테스트 +// *********************************************************** + +describe('리더보드 페이지', () => { + beforeEach(() => { + // 인증 토큰 설정 (로그인 건너뛰기) + cy.setAuthCookies(); + + // 리더보드 페이지 방문 + cy.visit('/leaderboards'); + }); + + it('페이지가 정상적으로 로드되어야 한다', () => { + cy.waitForPageLoad(); + cy.url().should('include', '/leaderboards'); + }); + + it('사용자 리더보드가 표시되어야 한다', () => { + // 사용자 리더보드 기본 선택 확인 (기본값이 '사용자 기준'이므로) + cy.get('select').first().should('have.value', '사용자 기준'); + + // 사용자 정보 확인 (이메일 앞부분 표시) + cy.contains('user1').should('be.visible'); + cy.contains('user2').should('be.visible'); + + // 통계 정보 확인 (viewDiff 값들) + cy.contains('500').should('be.visible'); // user1의 viewDiff + cy.contains('300').should('be.visible'); // user2의 viewDiff + }); + + it('게시물 리더보드가 표시되어야 한다', () => { + // 게시물 리더보드로 전환 + cy.get('select').first().select('게시글 기준'); + + // 게시물 정보 확인 + cy.contains('인기 게시물 1').should('be.visible'); + cy.contains('인기 게시물 2').should('be.visible'); + + // 통계 정보 확인 (viewDiff 값들) + cy.contains('200').should('be.visible'); // post1의 viewDiff + cy.contains('150').should('be.visible'); // post2의 viewDiff + }); + + it('필터 기능이 동작해야 한다', () => { + // 4개의 드롭다운 확인 (기준, 정렬, 개수, 기간) + cy.get('select').should('have.length', 4); + + // 정렬 옵션 변경 테스트 + cy.get('select').eq(1).select('좋아요 증가순'); + cy.get('select').eq(1).select('조회수 증가순'); + + // 개수 제한 변경 테스트 + cy.get('select').eq(2).select('30위까지'); + cy.get('select').eq(2).select('10위까지'); + + // 기간 필터 변경 테스트 + cy.get('select').eq(3).select('지난 7일'); + cy.get('select').eq(3).select('지난 30일'); + }); + + it('랭킹 순위가 표시되어야 한다', () => { + // 순위 표시 확인 + cy.get('[data-testid="rank"], [class*="rank"]').should('be.visible'); + cy.contains('1').should('be.visible'); // 1위 + cy.contains('2').should('be.visible'); // 2위 + }); + + it('통계 변화량이 표시되어야 한다', () => { + // 변화량 숫자 확인 (기본 사용자 리더보드, 조회수 기준) + cy.contains('500').should('be.visible'); // user1의 viewDiff + cy.contains('300').should('be.visible'); // user2의 viewDiff + cy.contains('250').should('be.visible'); // user3의 viewDiff + + // 좋아요 기준으로 변경 + cy.get('select').eq(1).select('좋아요 증가순'); + cy.contains('50').should('be.visible'); // user1의 likeDiff + cy.contains('40').should('be.visible'); // user2의 likeDiff + }); + + it('빈 데이터 상태를 올바르게 처리해야 한다', () => { + // 빈 데이터 응답을 모킹 + cy.intercept('GET', '**/api/leaderboard/user*', { + statusCode: 200, + body: { + success: true, + message: '사용자 리더보드 조회에 성공하였습니다.', + data: { + users: [], // 빈 배열 + }, + error: null, + }, + }).as('emptyUserLeaderboardAPI'); + + cy.intercept('GET', '**/api/leaderboard/post*', { + statusCode: 200, + body: { + success: true, + message: '게시물 리더보드 조회에 성공하였습니다.', + data: { + posts: [], // 빈 배열 + }, + error: null, + }, + }).as('emptyPostLeaderboardAPI'); + + // 페이지 새로고침하여 빈 데이터 모킹 적용 + cy.reload(); + + // 빈 데이터 상태 메시지 확인 + cy.contains('리더보드 데이터가 없습니다').should('be.visible'); + cy.contains('현재 설정된 조건에 맞는 사용자 데이터가 없습니다').should('be.visible'); + + // 게시물 리더보드로 전환하여 빈 데이터 상태 확인 + cy.get('select').first().select('게시글 기준'); + cy.contains('현재 설정된 조건에 맞는 게시물 데이터가 없습니다').should('be.visible'); + }); +}); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts new file mode 100644 index 0000000..c96e0e8 --- /dev/null +++ b/cypress/e2e/login.cy.ts @@ -0,0 +1,69 @@ +// *********************************************************** +// 로그인 페이지 E2E 테스트 +// *********************************************************** + +describe('로그인 페이지', () => { + beforeEach(() => { + // 로그인 페이지 방문 + cy.visit('/'); + }); + + it('페이지가 정상적으로 로드되어야 한다', () => { + cy.waitForPageLoad(); + cy.url().should('include', '/'); + }); + + it('로그인 폼이 존재해야 한다', () => { + cy.get('form').should('be.visible'); + cy.get( + 'input[name*="accessToken"], input[name*="access"], input[placeholder*="Access"]', + ).should('be.visible'); + cy.get( + 'input[name*="refreshToken"], input[name*="refresh"], input[placeholder*="Refresh"]', + ).should('be.visible'); + cy.get('button[type="submit"], button:contains("로그인")').should('be.visible'); + }); + + it('유효한 토큰으로 로그인할 수 있어야 한다', () => { + // 토큰 입력 + cy.get('input[name*="accessToken"], input[name*="access"], input[placeholder*="Access"]') + .first() + .type('valid_access_token'); + cy.get('input[name*="refreshToken"], input[name*="refresh"], input[placeholder*="Refresh"]') + .first() + .type('valid_refresh_token'); + + // 로그인 버튼 클릭 + cy.get('button[type="submit"], button:contains("로그인")').first().click(); + + // 메인 페이지로 리디렉션 확인 + cy.url().should('include', '/main'); + cy.waitForPageLoad(); + }); + + it('유효하지 않은 토큰으로 로그인 시 에러를 표시해야 한다', () => { + // 유효하지 않은 토큰 입력 + cy.get('input[name*="accessToken"], input[name*="access"], input[placeholder*="Access"]') + .first() + .type('invalid_token'); + cy.get('input[name*="refreshToken"], input[name*="refresh"], input[placeholder*="Refresh"]') + .first() + .type('invalid_token'); + + // 로그인 버튼 클릭 + cy.get('button[type="submit"], button:contains("로그인")').first().click(); + + // 401 에러 시 홈페이지로 리디렉트되는 것이 정상 동작 + // (instance.request.ts에서 401 에러 시 자동으로 '/'로 리디렉트) + cy.url().should('eq', Cypress.config().baseUrl + '/'); + }); + + it('샘플 로그인 버튼이 동작해야 한다', () => { + // 체험 계정 로그인 버튼 클릭 (실제 텍스트 사용) + cy.contains('체험 계정 로그인').should('be.visible').click(); + + // 메인 페이지로 리디렉션 확인 + cy.url().should('include', '/main'); + cy.waitForPageLoad(); + }); +}); diff --git a/cypress/e2e/main.cy.ts b/cypress/e2e/main.cy.ts new file mode 100644 index 0000000..6fa2119 --- /dev/null +++ b/cypress/e2e/main.cy.ts @@ -0,0 +1,102 @@ +// *********************************************************** +// 메인 페이지 E2E 테스트 +// *********************************************************** + +describe('메인 페이지', () => { + beforeEach(() => { + // 인증 토큰 설정 (로그인 건너뛰기) + cy.setAuthCookies(); + + // 메인 페이지 방문 + cy.visit('/main'); + }); + + it('페이지가 정상적으로 로드되어야 한다', () => { + cy.waitForPageLoad(); + cy.url().should('include', '/main'); + }); + + it('대시보드 통계 정보가 표시되어야 한다', () => { + // 사이드바 통계 확인 + cy.contains('전체 조회수').should('be.visible'); + cy.contains('전체 좋아요 수').should('be.visible'); + cy.contains('총 게시글 수').should('be.visible'); + + // 숫자 값 확인 + cy.contains('2500').should('be.visible'); // totalViews + cy.contains('350').should('be.visible'); // totalLikes + cy.contains('15').should('be.visible'); // totalPostCount + }); + + it('게시물 목록이 표시되어야 한다', () => { + // 게시물 제목 확인 + cy.contains('테스트 게시물 1').should('be.visible'); + cy.contains('테스트 게시물 2').should('be.visible'); + + // 게시물 통계 확인 + cy.contains('150').should('be.visible'); // 조회수 + cy.contains('25').should('be.visible'); // 좋아요 + }); + + it('정렬 및 필터 기능이 동작해야 한다', () => { + // 드롭다운 메뉴 확인 + cy.get('select').should('be.visible'); + + // 오름차순 체크박스 확인 + cy.get('input[type="checkbox"]').should('be.visible'); + + // 새로고침 버튼 확인 (비활성화 상태) + cy.contains('새로고침').should('be.visible'); + cy.contains('새로고침').should('be.disabled'); + }); + + it('마지막 업데이트 시간이 표시되어야 한다', () => { + // 업데이트 시간 텍스트 확인 + cy.contains('마지막 업데이트').should('be.visible'); + + // 날짜 형식 확인 (YYYY-MM-DD 형식이어야 함) + cy.contains(/\d{4}-\d{2}-\d{2}/).should('be.visible'); + }); + + it('로그아웃 기능이 동작해야 한다', () => { + // 프로필 클릭하여 드롭다운 열기 + cy.get('#profile').click(); + + // 로그아웃 버튼 확인 + cy.contains('로그아웃').should('be.visible'); + + // 로그아웃 클릭 + cy.contains('로그아웃').click(); + + // 로그인 페이지로 리다이렉트 확인 + cy.url().should('include', '/'); + }); + + it('빈 데이터 상태를 올바르게 처리해야 한다', () => { + // 빈 게시물 데이터 응답을 모킹 + cy.intercept('GET', '**/api/posts*', { + statusCode: 200, + body: { + success: true, + message: '게시물 목록 조회에 성공하였습니다.', + data: { + nextCursor: null, + posts: [], // 빈 배열 + }, + error: null, + }, + }).as('emptyPostsAPI'); + + // 페이지 새로고침하여 빈 데이터 모킹 적용 + cy.reload(); + + // 빈 데이터 상태 메시지 확인 + cy.contains('게시물이 없습니다').should('be.visible'); + cy.contains('아직 작성된 게시물이 없습니다. 첫 번째 게시물을 작성해보세요!').should( + 'be.visible', + ); + + // 📝 이모지 아이콘 확인 + cy.contains('📝').should('be.visible'); + }); +}); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 0000000..0e38d83 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,36 @@ +// *********************************************************** +// Cypress 커스텀 명령어 +// *********************************************************** + +import { MOCK_ACCESS_TOKEN, MOCK_REFRESH_TOKEN } from '../../src/__mock__/handlers'; + +// 인증 토큰을 쿠키에 설정하여 로그인 상태를 모킹 +Cypress.Commands.add('setAuthCookies', () => { + cy.setCookie('access_token', MOCK_ACCESS_TOKEN, { + httpOnly: true, + secure: true, + sameSite: 'strict', + path: '/', + }); + cy.setCookie('refresh_token', MOCK_REFRESH_TOKEN, { + httpOnly: true, + secure: true, + sameSite: 'strict', + path: '/', + }); + + // API 호출 확인을 위한 대기 + cy.wait(100); +}); + +// 인증 토큰을 쿠키에서 제거 +Cypress.Commands.add('clearAuthCookies', () => { + cy.clearCookie('access_token'); + cy.clearCookie('refresh_token'); +}); + +// 페이지 로드를 기다립니다 +Cypress.Commands.add('waitForPageLoad', () => { + cy.get('body').should('be.visible'); + cy.window().should('have.property', 'document'); +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 0000000..4eeccbc --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,376 @@ +// *********************************************************** +// Cypress E2E 지원 파일 +// *********************************************************** + +// Cypress 명령어 타입 정의 +import './commands'; + +// Cypress에서는 cy.intercept를 사용하여 API 모킹 +beforeEach(() => { + // 로그인 API 모킹 + cy.intercept('POST', '**/api/login', (req) => { + const body = req.body; + if (body.accessToken === 'invalid_token' || body.refreshToken === 'invalid_token') { + req.reply({ + statusCode: 401, + body: { + success: false, + message: '유효하지 않은 토큰입니다.', + data: null, + error: '유효하지 않은 토큰입니다.', + }, + }); + } else { + req.reply({ + statusCode: 200, + body: { + success: true, + message: '로그인에 성공하였습니다.', + data: { + id: 'user-1', + username: 'testuser', + email: 'test@example.com', + profile: { + thumbnail: 'https://example.com/avatar.png', + }, + }, + error: null, + }, + }); + } + }).as('loginAPI'); + + // 샘플 로그인 API 모킹 + cy.intercept('POST', '**/api/login-sample', { + statusCode: 200, + body: { + success: true, + message: '샘플 로그인에 성공하였습니다.', + data: { + id: 'user-999', + username: 'sampleuser', + email: 'sample@example.com', + profile: { + thumbnail: 'https://example.com/sample-avatar.png', + }, + }, + error: null, + }, + }).as('sampleLoginAPI'); + + // 사용자 정보 API 모킹 + cy.intercept('GET', '**/api/me', { + statusCode: 200, + body: { + success: true, + message: '사용자 정보 조회에 성공하였습니다.', + data: { + id: 'user-1', + username: 'testuser', + email: 'test@example.com', + profile: { + thumbnail: '/profile.jpg', + }, + }, + error: null, + }, + }).as('meAPI'); + + // 게시물 목록 API 모킹 (첫 번째 페이지) + cy.intercept('GET', '**/api/posts*', (req) => { + // URL에서 cursor 확인 + const url = new URL(req.url); + const cursor = url.searchParams.get('cursor'); + + if (!cursor) { + // 첫 번째 페이지 - nextCursor 포함 + req.reply({ + statusCode: 200, + body: { + success: true, + message: '게시물 목록 조회에 성공하였습니다.', + data: { + nextCursor: '2025-01-09T00:00:00Z,10', + posts: [ + { + id: 1, + title: '테스트 게시물 1', + slug: 'test-post-1', + views: 150, + likes: 25, + yesterdayViews: 10, + yesterdayLikes: 5, + createdAt: '2025-01-08T10:00:00Z', + releasedAt: '2025-01-08T10:00:00Z', + }, + { + id: 2, + title: '테스트 게시물 2', + slug: 'test-post-2', + views: 200, + likes: 35, + yesterdayViews: 15, + yesterdayLikes: 8, + createdAt: '2025-01-07T15:30:00Z', + releasedAt: '2025-01-07T15:30:00Z', + }, + ], + }, + error: null, + }, + }); + } else { + // 두 번째 페이지 - nextCursor null로 무한 스크롤 종료 + req.reply({ + statusCode: 200, + body: { + success: true, + message: '게시물 목록 조회에 성공하였습니다.', + data: { + nextCursor: null, + posts: [ + { + id: 3, + title: '테스트 게시물 3', + slug: 'test-post-3', + views: 120, + likes: 18, + yesterdayViews: 8, + yesterdayLikes: 3, + createdAt: '2025-01-06T09:00:00Z', + releasedAt: '2025-01-06T09:00:00Z', + }, + { + id: 4, + title: '테스트 게시물 4', + slug: 'test-post-4', + views: 90, + likes: 12, + yesterdayViews: 5, + yesterdayLikes: 2, + createdAt: '2025-01-05T14:00:00Z', + releasedAt: '2025-01-05T14:00:00Z', + }, + ], + }, + error: null, + }, + }); + } + }).as('postsAPI'); + + // 게시물 통계 API 모킹 + cy.intercept('GET', '**/api/posts-stats', { + statusCode: 200, + body: { + success: true, + message: '게시물 통계 조회에 성공하였습니다.', + data: { + totalPostCount: 15, + stats: { + lastUpdatedDate: '2025-01-09T00:00:00Z', + totalLikes: 350, + totalViews: 2500, + yesterdayLikes: 45, + yesterdayViews: 180, + }, + }, + error: null, + }, + }).as('postsStatsAPI'); + + // 사용자 리더보드 API 모킹 + cy.intercept('GET', '**/api/leaderboard/user*', { + statusCode: 200, + body: { + success: true, + message: '사용자 리더보드 조회에 성공하였습니다.', + data: { + users: [ + { + id: 'user-1', + email: 'user1@example.com', + username: 'topuser1', + totalViews: 15000, + totalLikes: 1200, + totalPosts: 45, + viewDiff: 500, + likeDiff: 50, + postDiff: 3, + }, + { + id: 'user-2', + email: 'user2@example.com', + username: 'topuser2', + totalViews: 12000, + totalLikes: 980, + totalPosts: 38, + viewDiff: 300, + likeDiff: 40, + postDiff: 2, + }, + { + id: 'user-3', + email: 'user3@example.com', + username: 'topuser3', + totalViews: 10000, + totalLikes: 800, + totalPosts: 30, + viewDiff: 250, + likeDiff: 35, + postDiff: 1, + }, + ], + }, + error: null, + }, + }).as('userLeaderboardAPI'); + + // 게시물 리더보드 API 모킹 + cy.intercept('GET', '**/api/leaderboard/post*', { + statusCode: 200, + body: { + success: true, + message: '게시물 리더보드 조회에 성공하였습니다.', + data: { + posts: [ + { + id: 'post-1', + title: '인기 게시물 1', + slug: 'popular-post-1', + username: 'author1', + totalViews: 5000, + totalLikes: 400, + viewDiff: 200, + likeDiff: 30, + releasedAt: '2025-01-07T10:00:00Z', + }, + { + id: 'post-2', + title: '인기 게시물 2', + slug: 'popular-post-2', + username: 'author2', + totalViews: 4500, + totalLikes: 350, + viewDiff: 150, + likeDiff: 25, + releasedAt: '2025-01-06T14:30:00Z', + }, + { + id: 'post-3', + title: '인기 게시물 3', + slug: 'popular-post-3', + username: 'author3', + totalViews: 4000, + totalLikes: 300, + viewDiff: 120, + likeDiff: 20, + releasedAt: '2025-01-05T09:15:00Z', + }, + ], + }, + error: null, + }, + }).as('postLeaderboardAPI'); + + // 전체 통계 API 모킹 + cy.intercept('GET', '**/api/total-stats*', { + statusCode: 200, + body: { + success: true, + message: '전체 통계 조회에 성공하였습니다.', + data: [ + { date: '2025-01-03T00:00:00Z', value: 100 }, + { date: '2025-01-04T00:00:00Z', value: 150 }, + { date: '2025-01-05T00:00:00Z', value: 200 }, + { date: '2025-01-06T00:00:00Z', value: 180 }, + { date: '2025-01-07T00:00:00Z', value: 250 }, + { date: '2025-01-08T00:00:00Z', value: 300 }, + { date: '2025-01-09T00:00:00Z', value: 350 }, + ], + error: null, + }, + }).as('totalStatsAPI'); + + // 공지사항 API 모킹 + cy.intercept('GET', '**/api/notis', { + statusCode: 200, + body: { + success: true, + message: '공지사항 조회에 성공하였습니다.', + data: { + posts: [ + { + id: 'noti-1', + title: '시스템 점검 안내', + content: '시스템 점검이 예정되어 있습니다.', + createdAt: '2025-01-08T09:00:00Z', + isImportant: true, + }, + { + id: 'noti-2', + title: '새로운 기능 업데이트', + content: '새로운 기능이 추가되었습니다.', + createdAt: '2025-01-07T16:00:00Z', + isImportant: false, + }, + ], + }, + error: null, + }, + }).as('notisAPI'); + + // 로그아웃 API 모킹 + cy.intercept('POST', '**/api/logout', { + statusCode: 200, + body: { + success: true, + message: '로그아웃에 성공하였습니다.', + data: {}, + error: null, + }, + }).as('logoutAPI'); + + // 게시물 상세 차트 API 모킹 (차트 데이터용) + cy.intercept('GET', '**/api/post/**', { + statusCode: 200, + body: { + success: true, + message: '게시물 상세 정보 조회에 성공하였습니다.', + data: { + post: [ + { date: '2025-01-03T00:00:00Z', dailyViewCount: 20, dailyLikeCount: 2 }, + { date: '2025-01-04T00:00:00Z', dailyViewCount: 35, dailyLikeCount: 5 }, + { date: '2025-01-05T00:00:00Z', dailyViewCount: 45, dailyLikeCount: 8 }, + { date: '2025-01-06T00:00:00Z', dailyViewCount: 30, dailyLikeCount: 4 }, + { date: '2025-01-07T00:00:00Z', dailyViewCount: 60, dailyLikeCount: 12 }, + { date: '2025-01-08T00:00:00Z', dailyViewCount: 80, dailyLikeCount: 15 }, + { date: '2025-01-09T00:00:00Z', dailyViewCount: 100, dailyLikeCount: 20 }, + ], + }, + error: null, + }, + }).as('postDetailAPI'); +}); + +// 전역 타입 선언 +declare global { + namespace Cypress { + interface Chainable { + /** + * 인증 토큰을 쿠키에 설정하여 로그인 상태를 모킹합니다. + */ + setAuthCookies(): Chainable; + + /** + * 인증 토큰을 쿠키에서 제거합니다. + */ + clearAuthCookies(): Chainable; + + /** + * 페이지 로드를 기다립니다. + */ + waitForPageLoad(): Chainable; + } + } +} diff --git a/package.json b/package.json index 8984a00..ce016ea 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,12 @@ "lint": "next lint", "lintTest": "eslint ./src/__test__", "format": "prettier --check --ignore-path .gitignore --ignore-path pnpm-lock.yaml .", - "test": "jest" + "test": "jest", + "cypress:open": "cypress open", + "cypress:run": "cypress run", + "cypress:test": "cypress run --headless", + "e2e:dev": "start-server-and-test dev http://localhost:3000 cypress:open", + "e2e:test": "start-server-and-test dev http://localhost:3000 cypress:test" }, "dependencies": { "@channel.io/channel-web-sdk-loader": "^2.0.0", @@ -43,6 +48,7 @@ "zustand": "^5.0.3" }, "devDependencies": { + "@cypress/webpack-preprocessor": "^6.0.4", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.15.0", "@sentry/webpack-plugin": "^3.4.0", @@ -61,6 +67,7 @@ "@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/parser": "^8.33.0", "babel-plugin-inline-react-svg": "^2.0.2", + "cypress": "^14.5.1", "eslint": "^8.57.1", "eslint-config-next": "14.2.18", "eslint-config-prettier": "^9.1.0", @@ -80,9 +87,15 @@ "msw": "^2.7.3", "postcss": "^8", "prettier": "^3.3.3", + "start-server-and-test": "^2.0.12", "tailwindcss": "^3.4.1", "ts-node": "^10.9.2", "typescript": "^5", "typescript-eslint": "^8.15.0" + }, + "msw": { + "workerDirectory": [ + "public" + ] } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d11744..4d541c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: specifier: ^5.0.3 version: 5.0.3(@types/react@18.3.18)(react@18.3.1) devDependencies: + '@cypress/webpack-preprocessor': + specifier: ^6.0.4 + version: 6.0.4(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(babel-loader@10.0.0(@babel/core@7.26.0)(webpack@5.97.1))(webpack@5.97.1) '@eslint/eslintrc': specifier: ^3.3.1 version: 3.3.1 @@ -129,6 +132,9 @@ importers: babel-plugin-inline-react-svg: specifier: ^2.0.2 version: 2.0.2(@babel/core@7.26.0) + cypress: + specifier: ^14.5.1 + version: 14.5.1 eslint: specifier: ^8.57.1 version: 8.57.1 @@ -186,6 +192,9 @@ importers: prettier: specifier: ^3.3.3 version: 3.4.2 + start-server-and-test: + specifier: ^2.0.12 + version: 2.0.12 tailwindcss: specifier: ^3.4.1 version: 3.4.17(ts-node@10.9.2(@types/node@20.17.10)(typescript@5.7.2)) @@ -849,6 +858,21 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@cypress/request@3.0.8': + resolution: {integrity: sha512-h0NFgh1mJmm1nr4jCwkGHwKneVYKghUyWe6TMNrk0B9zsjAJxpg8C4/+BAcmLgCPa1vj1V8rNUaILl+zYRUWBQ==} + engines: {node: '>= 6'} + + '@cypress/webpack-preprocessor@6.0.4': + resolution: {integrity: sha512-ly+EcabWWbhrSPr2J/njQX7Y3da+QqOmFg8Og/MVmLxhDLKIzr2WhTdgzDYviPTLx/IKsdb41cc2RLYp6mSBRA==} + peerDependencies: + '@babel/core': ^7.25.2 + '@babel/preset-env': ^7.25.3 + babel-loader: ^8.3 || ^9 || ^10 + webpack: ^4 || ^5 + + '@cypress/xvfb@1.2.4': + resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} + '@emnapi/runtime@1.4.3': resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} @@ -884,6 +908,12 @@ packages: resolution: {integrity: sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1666,6 +1696,15 @@ packages: peerDependencies: webpack: '>=4.40.0' + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -1922,6 +1961,12 @@ packages: '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/sinonjs__fake-timers@8.1.1': + resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} + + '@types/sizzle@2.3.9': + resolution: {integrity: sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -1940,6 +1985,9 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@8.18.2': resolution: {integrity: sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2173,6 +2221,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -2197,6 +2249,10 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -2232,6 +2288,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -2287,20 +2346,47 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + axe-core@4.10.2: resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -2311,6 +2397,13 @@ packages: peerDependencies: '@babel/core': ^7.8.0 + babel-loader@10.0.0: + resolution: {integrity: sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==} + engines: {node: ^18.20.0 || ^20.10.0 || >=22.0.0} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5.61.0' + babel-plugin-inline-react-svg@2.0.2: resolution: {integrity: sha512-iM9obPpCcdPE1EJE+UF+tni7CZ4q/OvdDm/TeBBHAYAEOqDcFd7fdnmym6OYAQMYfEpUnRYUYx2KxSUyo4cQxQ==} engines: {node: '>=10.13'} @@ -2354,10 +2447,25 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + blob-util@2.0.2: + resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} + + bluebird@3.7.1: + resolution: {integrity: sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -2379,13 +2487,23 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + cachedir@2.4.0: + resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} + engines: {node: '>=6'} + call-bind-apply-helpers@1.0.1: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} @@ -2417,6 +2535,9 @@ packages: caniuse-lite@1.0.30001690: resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==} + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} engines: {node: '>=8'} @@ -2442,6 +2563,10 @@ packages: peerDependencies: chart.js: '>=3.0.0' + check-more-types@2.24.0: + resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} + engines: {node: '>= 0.8.0'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2454,13 +2579,33 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + ci-info@4.3.0: + resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} + engines: {node: '>=8'} + cjs-module-lexer@1.4.1: resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-table3@0.6.1: + resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==} + engines: {node: 10.* || >= 12.*} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + cli-truncate@4.0.0: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} @@ -2504,6 +2649,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2519,10 +2668,18 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -2539,6 +2696,9 @@ packages: core-js-compat@3.39.0: resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -2614,9 +2774,18 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + cypress@14.5.1: + resolution: {integrity: sha512-vYBeZKW3UAtxwv5mFuSlOBCYhyO0H86TeDKRJ7TgARyHiREIaiDjeHtqjzrXRFrdz9KnNavqlm+z+hklC7v8XQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} @@ -2633,6 +2802,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2650,6 +2822,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} @@ -2763,9 +2944,15 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + electron-to-chromium@1.5.76: resolution: {integrity: sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==} @@ -2782,10 +2969,17 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.18.0: resolution: {integrity: sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==} engines: {node: '>=10.13.0'} + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} @@ -2838,6 +3032,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -3032,6 +3230,12 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-stream@3.3.4: + resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + + eventemitter2@6.4.7: + resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -3039,6 +3243,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -3047,6 +3255,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -3055,6 +3267,18 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3084,6 +3308,9 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.4.2: resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} peerDependencies: @@ -3092,6 +3319,10 @@ packages: picomatch: optional: true + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -3115,6 +3346,15 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -3122,6 +3362,9 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + form-data@4.0.1: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} @@ -3129,6 +3372,13 @@ packages: forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -3167,6 +3417,10 @@ packages: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -3182,6 +3436,12 @@ packages: get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + getos@3.2.1: + resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -3210,6 +3470,10 @@ packages: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -3271,6 +3535,10 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hasha@5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3297,10 +3565,18 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} + http-signature@1.4.0: + resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} + engines: {node: '>=0.10'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3318,6 +3594,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3349,6 +3628,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -3430,6 +3713,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -3487,6 +3774,13 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -3505,6 +3799,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -3695,6 +3992,9 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3706,6 +4006,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + jsdom@20.0.3: resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} engines: {node: '>=14'} @@ -3737,9 +4040,15 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -3749,6 +4058,13 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsprim@2.0.2: + resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} + engines: {'0': node >=0.6.0} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3767,6 +4083,10 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + lazy-ass@1.6.0: + resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} + engines: {node: '> 0.8'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -3787,6 +4107,15 @@ packages: engines: {node: '>=18.12.0'} hasBin: true + listr2@3.14.0: + resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} + engines: {node: '>=10.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + listr2@8.2.5: resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} engines: {node: '>=18.0.0'} @@ -3815,9 +4144,20 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -3856,6 +4196,9 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + map-stream@0.1.0: + resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4069,6 +4412,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ospath@1.2.2: + resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -4088,6 +4434,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -4136,6 +4486,15 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -4257,6 +4616,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -4265,6 +4628,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -4279,12 +4646,23 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.0.0: + resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + ps-tree@1.2.0: + resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} + engines: {node: '>= 0.10'} + hasBin: true + psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4297,6 +4675,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -4394,6 +4776,9 @@ packages: resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} hasBin: true + request-progress@3.0.0: + resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4445,6 +4830,10 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -4472,6 +4861,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -4574,6 +4966,14 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -4599,9 +4999,17 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split@0.3.3: + resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} @@ -4617,10 +5025,18 @@ packages: resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} engines: {node: '>=6'} + start-server-and-test@2.0.12: + resolution: {integrity: sha512-U6QiS5qsz+DN5RfJJrkAXdooxMDnLZ+n5nR8kaX//ZH19SilF6b58Z3zM9zTfrNIkJepzauHo4RceSgvgUSX9w==} + engines: {node: '>=16'} + hasBin: true + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + stream-combiner@0.0.4: + resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -4803,6 +5219,23 @@ packages: third-party-capital@1.0.20: resolution: {integrity: sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==} + throttleit@1.0.1: + resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -4814,6 +5247,10 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -4821,6 +5258,10 @@ packages: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -4865,6 +5306,12 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4885,6 +5332,10 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + type-fest@4.37.0: resolution: {integrity: sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==} engines: {node: '>=16'} @@ -4944,9 +5395,17 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unplugin@1.0.1: resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -4962,6 +5421,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -4973,10 +5436,19 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} + wait-on@8.0.3: + resolution: {integrity: sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==} + engines: {node: '>=12.0.0'} + hasBin: true + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -5119,6 +5591,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -5181,7 +5656,7 @@ snapshots: '@babel/traverse': 7.26.4 '@babel/types': 7.26.3 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5233,7 +5708,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -5937,7 +6412,7 @@ snapshots: '@babel/parser': 7.26.3 '@babel/template': 7.25.9 '@babel/types': 7.26.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5968,6 +6443,47 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@cypress/request@3.0.8': + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 4.0.1 + http-signature: 1.4.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.14.0 + safe-buffer: 5.2.1 + tough-cookie: 5.1.2 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + + '@cypress/webpack-preprocessor@6.0.4(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(babel-loader@10.0.0(@babel/core@7.26.0)(webpack@5.97.1))(webpack@5.97.1)': + dependencies: + '@babel/core': 7.26.0 + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + babel-loader: 10.0.0(@babel/core@7.26.0)(webpack@5.97.1) + bluebird: 3.7.1 + debug: 4.4.0(supports-color@8.1.1) + lodash: 4.17.21 + semver: 7.7.2 + webpack: 5.97.1 + transitivePeerDependencies: + - supports-color + + '@cypress/xvfb@1.2.4(supports-color@8.1.1)': + dependencies: + debug: 3.2.7(supports-color@8.1.1) + lodash.once: 4.1.1 + transitivePeerDependencies: + - supports-color + '@emnapi/runtime@1.4.3': dependencies: tslib: 2.8.1 @@ -5988,7 +6504,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -6002,7 +6518,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -6017,10 +6533,16 @@ snapshots: '@eslint/js@9.17.0': {} + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6524,7 +7046,7 @@ snapshots: '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.28.0 forwarded-parse: 2.1.2 - semver: 7.6.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color @@ -6669,7 +7191,7 @@ snapshots: '@types/shimmer': 1.2.0 import-in-the-middle: 1.12.0 require-in-the-middle: 7.4.0 - semver: 7.6.3 + semver: 7.7.2 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -6982,6 +7504,14 @@ snapshots: - encoding - supports-color + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -7271,6 +7801,10 @@ snapshots: '@types/shimmer@1.2.0': {} + '@types/sinonjs__fake-timers@8.1.1': {} + + '@types/sizzle@2.3.9': {} + '@types/stack-utils@2.0.3': {} '@types/statuses@2.0.5': {} @@ -7287,6 +7821,11 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 20.17.10 + optional: true + '@typescript-eslint/eslint-plugin@8.18.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -7327,7 +7866,7 @@ snapshots: '@typescript-eslint/types': 8.18.2 '@typescript-eslint/typescript-estree': 8.18.2(typescript@5.7.2) '@typescript-eslint/visitor-keys': 8.18.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 8.57.1 typescript: 5.7.2 transitivePeerDependencies: @@ -7339,7 +7878,7 @@ snapshots: '@typescript-eslint/types': 8.33.0 '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.7.2) '@typescript-eslint/visitor-keys': 8.33.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 8.57.1 typescript: 5.7.2 transitivePeerDependencies: @@ -7349,7 +7888,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.7.2) '@typescript-eslint/types': 8.33.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - typescript @@ -7382,7 +7921,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.18.2(typescript@5.7.2) '@typescript-eslint/utils': 8.18.2(eslint@8.57.1)(typescript@5.7.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.7.2) typescript: 5.7.2 @@ -7393,7 +7932,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.32.0(typescript@5.7.2) '@typescript-eslint/utils': 8.32.0(eslint@8.57.1)(typescript@5.7.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 8.57.1 ts-api-utils: 2.1.0(typescript@5.7.2) typescript: 5.7.2 @@ -7412,10 +7951,10 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.3 + semver: 7.7.2 tsutils: 3.21.0(typescript@5.7.2) optionalDependencies: typescript: 5.7.2 @@ -7426,11 +7965,11 @@ snapshots: dependencies: '@typescript-eslint/types': 8.18.2 '@typescript-eslint/visitor-keys': 8.18.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.2 ts-api-utils: 1.4.3(typescript@5.7.2) typescript: 5.7.2 transitivePeerDependencies: @@ -7440,11 +7979,11 @@ snapshots: dependencies: '@typescript-eslint/types': 8.32.0 '@typescript-eslint/visitor-keys': 8.32.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.2 ts-api-utils: 2.1.0(typescript@5.7.2) typescript: 5.7.2 transitivePeerDependencies: @@ -7456,7 +7995,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.7.2) '@typescript-eslint/types': 8.33.0 '@typescript-eslint/visitor-keys': 8.33.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -7628,10 +8167,15 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -7659,6 +8203,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -7686,6 +8232,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arch@2.2.0: {} + arg@4.1.3: {} arg@5.0.2: {} @@ -7768,16 +8316,40 @@ snapshots: get-intrinsic: 1.2.6 is-array-buffer: 3.0.5 + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + ast-types-flow@0.0.8: {} + astral-regex@2.0.0: {} + + async@3.2.6: {} + asynckit@0.4.0: {} + at-least-node@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + axe-core@4.10.2: {} + axios@1.10.0(debug@4.4.1): + dependencies: + follow-redirects: 1.15.9(debug@4.4.1) + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} babel-jest@29.7.0(@babel/core@7.26.0): @@ -7793,6 +8365,12 @@ snapshots: transitivePeerDependencies: - supports-color + babel-loader@10.0.0(@babel/core@7.26.0)(webpack@5.97.1): + dependencies: + '@babel/core': 7.26.0 + find-up: 5.0.0 + webpack: 5.97.1 + babel-plugin-inline-react-svg@2.0.2(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -7870,8 +8448,20 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + binary-extensions@2.3.0: {} + blob-util@2.0.2: {} + + bluebird@3.7.1: {} + + bluebird@3.7.2: {} + boolbase@1.0.0: {} brace-expansion@1.1.11: @@ -7898,12 +8488,21 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 + cachedir@2.4.0: {} + call-bind-apply-helpers@1.0.1: dependencies: es-errors: 1.3.0 @@ -7931,6 +8530,8 @@ snapshots: caniuse-lite@1.0.30001690: {} + caseless@0.12.0: {} + chalk@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -7953,6 +8554,8 @@ snapshots: dependencies: chart.js: 4.4.7 + check-more-types@2.24.0: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -7969,12 +8572,31 @@ snapshots: ci-info@3.9.0: {} + ci-info@4.3.0: {} + cjs-module-lexer@1.4.1: {} + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 + cli-table3@0.6.1: + dependencies: + string-width: 4.2.3 + optionalDependencies: + colors: 1.4.0 + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + cli-truncate@4.0.0: dependencies: slice-ansi: 5.0.0 @@ -8014,6 +8636,9 @@ snapshots: colorette@2.0.20: {} + colors@1.4.0: + optional: true + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -8024,8 +8649,12 @@ snapshots: commander@4.1.1: {} + commander@6.2.1: {} + commander@7.2.0: {} + common-tags@1.8.2: {} + commondir@1.0.1: {} concat-map@0.0.1: {} @@ -8038,6 +8667,8 @@ snapshots: dependencies: browserslist: 4.24.3 + core-util-is@1.0.2: {} + cosmiconfig@8.3.6(typescript@5.7.2): dependencies: import-fresh: 3.3.0 @@ -8131,8 +8762,59 @@ snapshots: csstype@3.1.3: {} + cypress@14.5.1: + dependencies: + '@cypress/request': 3.0.8 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.9 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.4.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + ci-info: 4.3.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.1 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.13 + debug: 4.4.0(supports-color@8.1.1) + enquirer: 2.4.1 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + hasha: 5.2.2 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.4.1) + lodash: 4.17.21 + log-symbols: 4.1.0 + minimist: 1.2.8 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + process: 0.11.10 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + semver: 7.7.2 + supports-color: 8.1.1 + tmp: 0.2.3 + tree-kill: 1.2.2 + untildify: 4.0.0 + yauzl: 2.10.0 + damerau-levenshtein@1.0.8: {} + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + data-urls@3.0.2: dependencies: abab: 2.0.6 @@ -8157,11 +8839,21 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - debug@3.2.7: + dayjs@1.11.13: {} + + debug@3.2.7(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + debug@4.4.0(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 - debug@4.4.0: + debug@4.4.1: dependencies: ms: 2.1.3 @@ -8268,8 +8960,15 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer@0.1.2: {} + eastasianwidth@0.2.0: {} + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + electron-to-chromium@1.5.76: {} emittery@0.13.1: {} @@ -8280,11 +8979,20 @@ snapshots: emoji-regex@9.2.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.18.0: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + entities@2.2.0: {} entities@4.5.0: {} @@ -8392,6 +9100,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} @@ -8430,7 +9140,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.16.1 resolve: 1.22.10 transitivePeerDependencies: @@ -8439,7 +9149,7 @@ snapshots: eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) enhanced-resolve: 5.18.0 eslint: 8.57.1 fast-glob: 3.3.2 @@ -8454,7 +9164,7 @@ snapshots: eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 8.33.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 @@ -8470,7 +9180,7 @@ snapshots: array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 - debug: 3.2.7 + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 @@ -8601,7 +9311,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -8661,10 +9371,34 @@ snapshots: esutils@2.0.3: {} + event-stream@3.3.4: + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.1.0 + pause-stream: 0.0.11 + split: 0.3.3 + stream-combiner: 0.0.4 + through: 2.3.8 + + eventemitter2@6.4.7: {} + eventemitter3@5.0.1: {} events@3.3.0: {} + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -8689,6 +9423,10 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + executable@4.1.1: + dependencies: + pify: 2.3.0 + exit@0.1.2: {} expect@29.7.0: @@ -8699,6 +9437,20 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + extend@3.0.2: {} + + extract-zip@2.0.1(supports-color@8.1.1): + dependencies: + debug: 4.4.0(supports-color@8.1.1) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.3.0: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -8733,10 +9485,18 @@ snapshots: dependencies: bser: 2.1.1 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.4.2(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -8763,6 +9523,10 @@ snapshots: flatted@3.3.2: {} + follow-redirects@1.15.9(debug@4.4.1): + optionalDependencies: + debug: 4.4.1 + for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -8772,6 +9536,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forever-agent@0.6.1: {} + form-data@4.0.1: dependencies: asynckit: 0.4.0 @@ -8780,6 +9546,15 @@ snapshots: forwarded-parse@2.1.2: {} + from@0.1.7: {} + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -8819,6 +9594,10 @@ snapshots: get-package-type@0.1.0: {} + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -8833,6 +9612,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + getos@3.2.1: + dependencies: + async: 3.2.6 + + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -8876,6 +9663,10 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + globals@11.12.0: {} globals@13.24.0: @@ -8926,6 +9717,11 @@ snapshots: dependencies: has-symbols: 1.1.0 + hasha@5.2.2: + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -8950,17 +9746,25 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color + http-signature@1.4.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 2.0.2 + sshpk: 1.18.0 + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color + human-signals@1.1.1: {} + human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -8971,6 +9775,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} import-fresh@3.3.0: @@ -9001,6 +9807,8 @@ snapshots: inherits@2.0.4: {} + ini@2.0.0: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -9036,7 +9844,7 @@ snapshots: is-bun-module@1.3.0: dependencies: - semver: 7.6.3 + semver: 7.7.2 is-callable@1.2.7: {} @@ -9079,6 +9887,11 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + is-map@2.0.3: {} is-node-process@1.2.0: {} @@ -9130,6 +9943,10 @@ snapshots: dependencies: which-typed-array: 1.1.18 + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.0: @@ -9145,6 +9962,8 @@ snapshots: isexe@2.0.0: {} + isstream@0.1.2: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -9175,7 +9994,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -9490,7 +10309,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color @@ -9550,6 +10369,14 @@ snapshots: jiti@1.21.7: {} + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -9561,6 +10388,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsbn@0.1.1: {} + jsdom@20.0.3: dependencies: abab: 2.0.6 @@ -9606,14 +10435,31 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: {} + json5@1.0.2: dependencies: minimist: 1.2.8 json5@2.2.3: {} + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsprim@2.0.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -9633,6 +10479,8 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lazy-ass@1.6.0: {} + leven@3.1.0: {} levn@0.4.1: @@ -9648,7 +10496,7 @@ snapshots: dependencies: chalk: 5.4.1 commander: 13.1.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.2.5 @@ -9659,6 +10507,19 @@ snapshots: transitivePeerDependencies: - supports-color + listr2@3.14.0(enquirer@2.4.1): + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.2 + through: 2.3.8 + wrap-ansi: 7.0.0 + optionalDependencies: + enquirer: 2.4.1 + listr2@8.2.5: dependencies: cli-truncate: 4.0.0 @@ -9686,8 +10547,22 @@ snapshots: lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash@4.17.21: {} + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + log-update@6.1.0: dependencies: ansi-escapes: 7.0.0 @@ -9730,6 +10605,8 @@ snapshots: dependencies: tmpl: 1.0.5 + map-stream@0.1.0: {} + math-intrinsics@1.1.0: {} mdn-data@2.0.14: {} @@ -9945,6 +10822,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ospath@1.2.2: {} + outvariant@1.4.3: {} p-limit@2.3.0: @@ -9963,6 +10842,10 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -10001,6 +10884,14 @@ snapshots: path-type@4.0.0: {} + pause-stream@0.0.11: + dependencies: + through: 2.3.8 + + pend@1.2.0: {} + + performance-now@2.1.0: {} + pg-int8@1.0.1: {} pg-protocol@1.7.0: {} @@ -10098,6 +10989,8 @@ snapshots: prettier@3.4.2: {} + pretty-bytes@5.6.0: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -10110,6 +11003,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + process@0.11.10: {} + progress@2.0.3: {} promise-polyfill@8.3.0: {} @@ -10125,12 +11020,23 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-from-env@1.0.0: {} + proxy-from-env@1.1.0: {} + ps-tree@1.2.0: + dependencies: + event-stream: 3.3.4 + psl@1.15.0: dependencies: punycode: 2.3.1 + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -10139,6 +11045,10 @@ snapshots: dependencies: react: 18.3.1 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -10242,13 +11152,17 @@ snapshots: dependencies: jsesc: 3.0.2 + request-progress@3.0.0: + dependencies: + throttleit: 1.0.1 + require-directory@2.1.1: {} require-from-string@2.0.2: {} require-in-the-middle@7.4.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) module-details-from-path: 1.0.3 resolve: 1.22.10 transitivePeerDependencies: @@ -10288,6 +11202,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -10311,6 +11230,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -10451,6 +11374,18 @@ snapshots: slash@3.0.0: {} + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.1 @@ -10480,8 +11415,24 @@ snapshots: source-map@0.6.1: {} + split@0.3.3: + dependencies: + through: 2.3.8 + sprintf-js@1.0.3: {} + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + stable-hash@0.0.4: {} stable@0.1.8: {} @@ -10494,8 +11445,25 @@ snapshots: dependencies: type-fest: 0.7.1 + start-server-and-test@2.0.12: + dependencies: + arg: 5.0.2 + bluebird: 3.7.2 + check-more-types: 2.24.0 + debug: 4.4.1 + execa: 5.1.1 + lazy-ass: 1.6.0 + ps-tree: 1.2.0 + wait-on: 8.0.3(debug@4.4.1) + transitivePeerDependencies: + - supports-color + statuses@2.0.1: {} + stream-combiner@0.0.4: + dependencies: + duplexer: 0.1.2 + streamsearch@1.1.0: {} strict-event-emitter@0.5.1: {} @@ -10718,6 +11686,18 @@ snapshots: third-party-capital@1.0.20: {} + throttleit@1.0.1: {} + + through@2.3.8: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tmp@0.2.3: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -10731,12 +11711,18 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + tr46@0.0.3: {} tr46@3.0.0: dependencies: punycode: 2.3.1 + tree-kill@1.2.2: {} + ts-api-utils@1.4.3(typescript@5.7.2): dependencies: typescript: 5.7.2 @@ -10781,6 +11767,12 @@ snapshots: tslib: 1.14.1 typescript: 5.7.2 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tweetnacl@0.14.5: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -10793,6 +11785,8 @@ snapshots: type-fest@0.7.1: {} + type-fest@0.8.1: {} + type-fest@4.37.0: {} typed-array-buffer@1.0.3: @@ -10862,6 +11856,8 @@ snapshots: universalify@0.2.0: {} + universalify@2.0.1: {} + unplugin@1.0.1: dependencies: acorn: 8.14.0 @@ -10869,6 +11865,8 @@ snapshots: webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 + untildify@4.0.0: {} + update-browserslist-db@1.1.1(browserslist@4.24.3): dependencies: browserslist: 4.24.3 @@ -10886,6 +11884,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@8.3.2: {} + uuid@9.0.1: {} v8-compile-cache-lib@3.0.1: {} @@ -10896,10 +11896,26 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + w3c-xmlserializer@4.0.0: dependencies: xml-name-validator: 4.0.0 + wait-on@8.0.3(debug@4.4.1): + dependencies: + axios: 1.10.0(debug@4.4.1) + joi: 17.13.3 + lodash: 4.17.21 + minimist: 1.2.8 + rxjs: 7.8.2 + transitivePeerDependencies: + - debug + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -11068,6 +12084,11 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + yn@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000..34057e8 --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,307 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.7.3' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/src/__mock__/browser.ts b/src/__mock__/browser.ts new file mode 100644 index 0000000..b4be7a3 --- /dev/null +++ b/src/__mock__/browser.ts @@ -0,0 +1,28 @@ +import { setupWorker } from 'msw/browser'; +import { handlers } from './handlers'; + +// MSW 워커 설정 +export const worker = setupWorker(...handlers); + +// 브라우저에서 MSW 시작 +export const startWorker = async () => { + if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') { + await worker.start({ + onUnhandledRequest: 'bypass', + }); + } +}; + +// 브라우저에서 MSW 중지 +export const stopWorker = () => { + if (typeof window !== 'undefined') { + worker.stop(); + } +}; + +// 브라우저에서 MSW 리셋 +export const resetWorker = () => { + if (typeof window !== 'undefined') { + worker.resetHandlers(); + } +}; diff --git a/src/__mock__/handlers.ts b/src/__mock__/handlers.ts index f0f6f17..a1fd32a 100644 --- a/src/__mock__/handlers.ts +++ b/src/__mock__/handlers.ts @@ -1,74 +1,448 @@ -import { http } from 'msw'; -import { ENVS, PATHS } from '@/constants'; -import { LoginVo } from '@/types'; -import { BaseError, BaseSuccess } from './responses'; +import { http, HttpResponse } from 'msw'; +import { PATHS } from '@/constants'; +import { BaseSuccess, BaseError, UnauthorizedError, checkAuthTokens } from './responses'; -const BASE_URL = ENVS.BASE_URL + '/api'; +// 테스트 환경에서 BASE_URL 설정 +const BASE_URL = + typeof window !== 'undefined' + ? 'http://localhost:3000/api' // 브라우저 환경 (Cypress) + : (process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000') + '/api'; // Node.js 환경 +// 모킹용 유효한 토큰 +export const MOCK_ACCESS_TOKEN = 'mock_access_token_12345'; +export const MOCK_REFRESH_TOKEN = 'mock_refresh_token_67890'; + +// 사용자 로그인 const login = http.post(`${BASE_URL}${PATHS.LOGIN}`, async ({ request }) => { - const { accessToken, refreshToken } = (await request.json()) as LoginVo; - if (accessToken === 'invalid_access' && refreshToken === 'invalid_refresh') { - return BaseError(404, '잘못된 토큰입니다'); + const body = (await request.json()) as { accessToken: string; refreshToken: string }; + + // 유효하지 않은 토큰 시뮬레이션 + if (body.accessToken === 'invalid_token' || body.refreshToken === 'invalid_token') { + return BaseError(401, '유효하지 않은 토큰입니다.'); + } + + // 성공 응답 + const response = BaseSuccess( + { + id: 1, + username: 'testuser', + profile: { + thumbnail: 'https://example.com/avatar.png', + }, + }, + '로그인에 성공하였습니다.', + ); + + // 쿠키 설정 + response.headers.append( + 'Set-Cookie', + `access_token=${MOCK_ACCESS_TOKEN}; Path=/; HttpOnly; Secure; SameSite=Strict`, + ); + response.headers.append( + 'Set-Cookie', + `refresh_token=${MOCK_REFRESH_TOKEN}; Path=/; HttpOnly; Secure; SameSite=Strict`, + ); + + return response; +}); + +// 샘플 로그인 +const sampleLogin = http.post(`${BASE_URL}${PATHS.SAMPLELOGIN}`, async () => { + const response = BaseSuccess( + { + id: 999, + username: 'sampleuser', + profile: { + thumbnail: 'https://example.com/sample-avatar.png', + }, + }, + '샘플 로그인에 성공하였습니다.', + ); + + response.headers.append( + 'Set-Cookie', + `access_token=${MOCK_ACCESS_TOKEN}; Path=/; HttpOnly; Secure; SameSite=Strict`, + ); + response.headers.append( + 'Set-Cookie', + `refresh_token=${MOCK_REFRESH_TOKEN}; Path=/; HttpOnly; Secure; SameSite=Strict`, + ); + + return response; +}); + +// 로그아웃 +const logout = http.post(`${BASE_URL}${PATHS.LOGOUT}`, async ({ request }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); } - return BaseSuccess(null); + + const response = BaseSuccess({}, '로그아웃에 성공하였습니다.'); + + // 쿠키 삭제 + response.headers.append( + 'Set-Cookie', + `access_token=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0`, + ); + response.headers.append( + 'Set-Cookie', + `refresh_token=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0`, + ); + + return response; }); -const summary = http.get(`${BASE_URL}${PATHS.SUMMARY}`, async ({ request: { headers } }) => { - if (!headers.get('access_token') && !headers.get('refresh_token')) { - return BaseError(401, '잘못된 토큰입니다'); +// 현재 사용자 정보 +const me = http.get(`${BASE_URL}${PATHS.ME}`, async ({ request }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); } - return BaseSuccess({ - stats: { - lastUpdatedDate: '2025-01-09T00:00:00Z102', - totalLikes: 100, - totalViews: 100, - yesterdayLikes: 50, - yesterdayViews: 50, + + return BaseSuccess( + { + id: 'user-1', + username: 'testuser', + email: 'test@example.com', + profile: { + thumbnail: '/profile.jpg', + }, }, - totalPostCount: 10, - }); + '사용자 정보 조회에 성공하였습니다.', + ); +}); + +// QR 로그인 토큰 생성 +const createQRToken = http.post(`${BASE_URL}${PATHS.QRLOGIN}`, async ({ request }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); + } + + return BaseSuccess({ token: 'qr_token_12345' }, 'QR 로그인 토큰이 생성되었습니다.'); +}); + +// QR 로그인 토큰 조회 +const getQRToken = http.get(`${BASE_URL}${PATHS.QRLOGIN}`, async ({ request }) => { + const url = new URL(request.url); + const token = url.searchParams.get('token'); + + if (!token || token !== 'qr_token_12345') { + return BaseError(404, '만료되었거나 존재하지 않는 토큰입니다.'); + } + + return HttpResponse.redirect('/main', 302); }); -const me = http.get(`${BASE_URL}${PATHS.ME}`, async ({ request: { headers } }) => { - if (!headers.get('access_token') && !headers.get('refresh_token')) { - return BaseError(401, '잘못된 토큰입니다'); +// 게시물 목록 조회 +const posts = http.get(`${BASE_URL}${PATHS.POSTS}`, async ({ request }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); } - return BaseSuccess({ - email: 'test@test.com', - id: '111111-111111-111111-111111', - profile: { thumbnail: undefined }, - username: 'test', - }); + + const url = new URL(request.url); + const cursor = url.searchParams.get('cursor'); + + return BaseSuccess( + { + nextCursor: cursor ? null : '2025-01-09T00:00:00Z,10', + posts: [ + { + id: 1, + title: '테스트 게시물 1', + slug: 'test-post-1', + views: 150, + likes: 25, + yesterdayViews: 10, + yesterdayLikes: 5, + createdAt: '2025-01-08T10:00:00Z', + releasedAt: '2025-01-08T10:00:00Z', + }, + { + id: 2, + title: '테스트 게시물 2', + slug: 'test-post-2', + views: 200, + likes: 35, + yesterdayViews: 15, + yesterdayLikes: 8, + createdAt: '2025-01-07T15:30:00Z', + releasedAt: '2025-01-07T15:30:00Z', + }, + ], + }, + '게시물 목록 조회에 성공하였습니다.', + ); }); -const posts = http.get(`${BASE_URL}${PATHS.POSTS}`, async ({ request: { headers } }) => { - if (!headers.get('access_token') && !headers.get('refresh_token')) { - return BaseError(401, '잘못된 토큰입니다'); +// 게시물 통계 +const postsStats = http.get(`${BASE_URL}${PATHS.SUMMARY}`, async ({ request }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); } - return BaseSuccess({ - nextCursor: '2025-01-09T00:00:00Z102,10', - posts: [ + + return BaseSuccess( + { + totalPostCount: 15, + stats: { + lastUpdatedDate: '2025-01-09T00:00:00Z', + totalLikes: 350, + totalViews: 2500, + yesterdayLikes: 45, + yesterdayViews: 180, + }, + }, + '게시물 통계 조회에 성공하였습니다.', + ); +}); + +// 게시물 상세 조회 (ID 기반) +const postDetail = http.get(`${BASE_URL}${PATHS.DETAIL}/:postId`, async ({ request, params }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); + } + + const postId = params.postId as string; + + return BaseSuccess( + { + post: { + id: parseInt(postId), + title: `테스트 게시물 ${postId}`, + slug: `test-post-${postId}`, + views: 250, + likes: 40, + yesterdayViews: 20, + yesterdayLikes: 8, + createdAt: '2025-01-08T10:00:00Z', + releasedAt: '2025-01-08T10:00:00Z', + stats: [ + { date: '2025-01-07T00:00:00Z', views: 100, likes: 15 }, + { date: '2025-01-08T00:00:00Z', views: 150, likes: 25 }, + ], + }, + }, + '게시물 상세 조회에 성공하였습니다.', + ); +}); + +// 게시물 상세 조회 (UUID 기반) +const postByUUID = http.get( + `${BASE_URL}${PATHS.DETAIL}/velog/:postId`, + async ({ request, params }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); + } + + const postId = params.postId as string; + + return BaseSuccess( { - createdAt: '2025-01-01T01:01:01Z103', - id: '1', - likes: 100, - releasedAt: '2025-01-09T00:00:00Z102', - title: 'test title', - views: 100, - yesterdayLikes: 100, - yesterdayViews: 100, + post: { + id: postId, + title: `UUID 기반 테스트 게시물`, + slug: `uuid-test-post`, + views: 300, + likes: 50, + yesterdayViews: 25, + yesterdayLikes: 10, + createdAt: '2025-01-08T10:00:00Z', + releasedAt: '2025-01-08T10:00:00Z', + stats: [ + { date: '2025-01-07T00:00:00Z', views: 150, likes: 25 }, + { date: '2025-01-08T00:00:00Z', views: 175, likes: 35 }, + ], + }, }, + 'UUID 기반 게시물 상세 조회에 성공하였습니다.', + ); + }, +); + +// 사용자 리더보드 +const userLeaderboard = http.get(`${BASE_URL}${PATHS.LEADERBOARD}/user`, async ({ request }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); + } + + return BaseSuccess( + { + users: [ + { + id: 'user-1', + email: 'user1@example.com', + username: 'topuser1', + totalViews: 15000, + totalLikes: 1200, + totalPosts: 45, + viewDiff: 500, + likeDiff: 50, + postDiff: 3, + }, + { + id: 'user-2', + email: 'user2@example.com', + username: 'topuser2', + totalViews: 12000, + totalLikes: 980, + totalPosts: 38, + viewDiff: 300, + likeDiff: 40, + postDiff: 2, + }, + { + id: 'user-3', + email: 'user3@example.com', + username: 'topuser3', + totalViews: 10000, + totalLikes: 800, + totalPosts: 30, + viewDiff: 250, + likeDiff: 35, + postDiff: 1, + }, + ], + }, + '사용자 리더보드 조회에 성공하였습니다.', + ); +}); + +// 게시물 리더보드 +const postLeaderboard = http.get(`${BASE_URL}${PATHS.LEADERBOARD}/post`, async ({ request }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); + } + + return BaseSuccess( + { + posts: [ + { + id: 'post-1', + title: '인기 게시물 1', + slug: 'popular-post-1', + username: 'author1', + totalViews: 5000, + totalLikes: 400, + viewDiff: 200, + likeDiff: 30, + releasedAt: '2025-01-07T10:00:00Z', + }, + { + id: 'post-2', + title: '인기 게시물 2', + slug: 'popular-post-2', + username: 'author2', + totalViews: 4500, + totalLikes: 350, + viewDiff: 150, + likeDiff: 25, + releasedAt: '2025-01-06T14:30:00Z', + }, + { + id: 'post-3', + title: '인기 게시물 3', + slug: 'popular-post-3', + username: 'author3', + totalViews: 4000, + totalLikes: 300, + viewDiff: 120, + likeDiff: 20, + releasedAt: '2025-01-05T09:15:00Z', + }, + ], + }, + '게시물 리더보드 조회에 성공하였습니다.', + ); +}); + +// 전체 통계 +const totalStats = http.get(`${BASE_URL}${PATHS.TOTALSTATS}`, async ({ request }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); + } + + const url = new URL(request.url); + const type = url.searchParams.get('type') || 'view'; + + const getMessage = (type: string) => { + switch (type) { + case 'view': + return '전체 조회수 변동 조회에 성공하였습니다.'; + case 'like': + return '전체 좋아요 변동 조회에 성공하였습니다.'; + case 'post': + return '전체 게시글 수 변동 조회에 성공하였습니다.'; + default: + return '전체 통계 조회에 성공하였습니다.'; + } + }; + + return BaseSuccess( + [ + { date: '2025-01-03T00:00:00Z', value: 100 }, + { date: '2025-01-04T00:00:00Z', value: 150 }, + { date: '2025-01-05T00:00:00Z', value: 200 }, + { date: '2025-01-06T00:00:00Z', value: 180 }, + { date: '2025-01-07T00:00:00Z', value: 250 }, + { date: '2025-01-08T00:00:00Z', value: 300 }, + { date: '2025-01-09T00:00:00Z', value: 350 }, ], - }); + getMessage(type), + ); }); -export const handlers = [login, summary, me, posts]; +// 공지사항 +const notifications = http.get(`${BASE_URL}${PATHS.NOTIS}`, async ({ request }) => { + if (!checkAuthTokens(request.headers)) { + return UnauthorizedError('인증이 필요합니다.'); + } + + return BaseSuccess( + { + posts: [ + { + id: 'noti-1', + title: '시스템 점검 안내', + content: '시스템 점검이 예정되어 있습니다.', + createdAt: '2025-01-08T09:00:00Z', + isImportant: true, + }, + { + id: 'noti-2', + title: '새로운 기능 업데이트', + content: '새로운 기능이 추가되었습니다.', + createdAt: '2025-01-07T16:00:00Z', + isImportant: false, + }, + ], + }, + '공지사항 조회에 성공하였습니다.', + ); +}); + +// Sentry 웹훅 (테스트용) +const sentryWebhook = http.post(`${BASE_URL}/webhook/sentry`, async ({ request }) => { + const body = (await request.json()) as { action: string; data?: unknown; actor?: unknown }; + + if (body.action !== 'created') { + return BaseError(400, 'Sentry 웹훅 처리에 실패했습니다.'); + } + + return BaseSuccess({}, 'Sentry 웹훅 처리에 성공하였습니다.'); +}); -export const tmp = { - LOGIN: '/login', - POSTS: '/posts', - SUMMARY: '/posts-stats', - ME: '/me', - LOGOUT: '/logout', - DETAIL: '/post', -}; +export const handlers = [ + login, + sampleLogin, + logout, + me, + createQRToken, + getQRToken, + posts, + postsStats, + postDetail, + postByUUID, + userLeaderboard, + postLeaderboard, + totalStats, + notifications, + sentryWebhook, +]; diff --git a/src/__mock__/responses.ts b/src/__mock__/responses.ts index 2a8a3be..36abeeb 100644 --- a/src/__mock__/responses.ts +++ b/src/__mock__/responses.ts @@ -1,21 +1,58 @@ import { HttpResponse } from 'msw'; -export const BaseSuccess = (data: object | null) => - HttpResponse.json( - { success: true, message: '성공적으로 동작하였습니다', data, error: null }, +// API 프로젝트의 BaseResponseDto 형식에 맞춘 응답 타입 +export interface BaseResponseDto { + success: boolean; + message: string; + data: T; + error: string | null; +} + +// 성공 응답 +export const BaseSuccess = ( + data: T, + message: string = '성공적으로 처리되었습니다.', +): HttpResponse => + HttpResponse.json>( + { + success: true, + message, + data, + error: null, + }, { status: 200 }, ); -export const BaseError = (code: number, message: string) => - HttpResponse.json( +// 에러 응답 +export const BaseError = (statusCode: number, message: string): HttpResponse => + HttpResponse.json>( { success: false, message, data: null, - error: { - code: message, - statusCode: code, - }, + error: message, + }, + { + status: statusCode, + statusText: message, }, - { status: code, statusText: message }, ); + +// 인증 실패 응답 +export const UnauthorizedError = (message: string = '인증이 필요합니다.'): HttpResponse => + BaseError(401, message); + +// 잘못된 요청 응답 +export const BadRequestError = (message: string = '잘못된 요청입니다.'): HttpResponse => + BaseError(400, message); + +// 서버 오류 응답 +export const InternalServerError = ( + message: string = '서버 내부 오류가 발생했습니다.', +): HttpResponse => BaseError(500, message); + +// 인증 토큰 확인 유틸리티 +export const checkAuthTokens = (headers: Headers): boolean => { + const cookies = headers.get('cookie') || ''; + return cookies.includes('access_token') && cookies.includes('refresh_token'); +}; diff --git a/src/__mock__/server.ts b/src/__mock__/server.ts index cae7e98..8d43b9c 100644 --- a/src/__mock__/server.ts +++ b/src/__mock__/server.ts @@ -1,7 +1,38 @@ -import { setupServer } from 'msw/node'; +// MSW 서버 설정 (Node.js 환경에서만 사용) +export const createMSWServer = async () => { + if (typeof window !== 'undefined') { + throw new Error('MSW server should only be used in Node.js environment'); + } -import { handlers } from './handlers'; + const { setupServer } = await import('msw/node'); + const { handlers } = await import('./handlers'); -const server = setupServer(...handlers); + return setupServer(...handlers); +}; -export default server; +// Cypress에서 사용할 서버 인스턴스 +let serverInstance: Awaited> | null = null; + +// 테스트 환경에서 서버 시작 +export const startServer = async () => { + if (!serverInstance) { + serverInstance = await createMSWServer(); + } + serverInstance.listen({ + onUnhandledRequest: 'error', + }); +}; + +// 테스트 환경에서 서버 중지 +export const stopServer = () => { + if (serverInstance) { + serverInstance.close(); + } +}; + +// 테스트 환경에서 서버 리셋 +export const resetServer = () => { + if (serverInstance) { + serverInstance.resetHandlers(); + } +}; diff --git a/src/app/msw-provider.tsx b/src/app/msw-provider.tsx new file mode 100644 index 0000000..341e38e --- /dev/null +++ b/src/app/msw-provider.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface MSWProviderProps { + children: React.ReactNode; +} + +export default function MSWProvider({ children }: MSWProviderProps) { + const [mswReady, setMswReady] = useState(false); + + useEffect(() => { + const init = async () => { + if (typeof window !== 'undefined') { + if (process.env.NODE_ENV === 'development') { + const { worker } = await import('../__mock__/browser'); + await worker.start({ + onUnhandledRequest: 'bypass', + quiet: true, + }); + } + } + setMswReady(true); + }; + + init(); + }, []); + + if (!mswReady) { + return null; + } + + return <>{children}; +} From 2f848bfa48e514823e3836a7a3edf0936923bae8 Mon Sep 17 00:00:00 2001 From: six-standard Date: Sun, 13 Jul 2025 07:55:19 +0900 Subject: [PATCH 06/14] =?UTF-8?q?refactor:=201=EC=B0=A8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(auth-required)/leaderboards/Content.tsx | 50 ++++++++++++++----- src/app/(auth-required)/main/Content.tsx | 25 ++++++++-- src/app/components/Header/index.tsx | 6 +-- src/constants/env.constant.ts | 3 +- src/shared/EmptyState.tsx | 17 +++++++ src/shared/index.ts | 1 + 6 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 src/shared/EmptyState.tsx diff --git a/src/app/(auth-required)/leaderboards/Content.tsx b/src/app/(auth-required)/leaderboards/Content.tsx index c28c2fb..5b9fa32 100644 --- a/src/app/(auth-required)/leaderboards/Content.tsx +++ b/src/app/(auth-required)/leaderboards/Content.tsx @@ -7,7 +7,7 @@ import { leaderboardList } from '@/apis'; import { Rank } from '@/app/components'; import { PATHS } from '@/constants'; import { useSearchParam } from '@/hooks'; -import { Dropdown } from '@/shared'; +import { Dropdown, EmptyState } from '@/shared'; import { LeaderboardItemType } from '@/types'; export type searchParamsType = { @@ -20,28 +20,44 @@ export type searchParamsType = { export const Content = () => { const [searchParams, setSearchParams] = useSearchParam(); - const { data: boards } = useQuery({ - queryKey: [PATHS.LEADERBOARD, searchParams], - queryFn: async () => await leaderboardList(searchParams), + // 기본값 설정 + const defaultParams = { + based: 'user' as const, + sort: 'viewCount' as const, + limit: '10', + dateRange: '30', + }; + + const finalParams = { + ...defaultParams, + ...searchParams, + }; + + const { data: boards, isLoading } = useQuery({ + queryKey: [PATHS.LEADERBOARD, finalParams], + queryFn: async () => await leaderboardList(finalParams), }); const data = useMemo(() => { const value = ( - searchParams.based === 'user' ? boards?.users : boards?.posts + finalParams.based === 'user' ? boards?.users : boards?.posts ) as LeaderboardItemType[]; return ( - value.map((item) => ({ - name: searchParams.based === 'user' ? item.email.split('@')[0] : item.title, - value: searchParams.sort === 'viewCount' ? item.viewDiff : item.likeDiff, + value?.map((item) => ({ + name: finalParams.based === 'user' ? item.email.split('@')[0] : item.title, + value: finalParams.sort === 'viewCount' ? item.viewDiff : item.likeDiff, })) || [] ); - }, [boards, searchParams.based, searchParams.sort]); + }, [boards, finalParams.based, finalParams.sort]); const handleChange = (param: Partial) => { startHolyLoader(); setSearchParams(param); }; + // 로딩 중이 아니고 데이터가 없는 경우 + const isEmpty = !isLoading && data.length === 0; + return (
@@ -72,7 +88,7 @@ export const Content = () => { ['30위까지', '30'], ]} onChange={(data) => handleChange({ limit: data as string })} - defaultValue={`${searchParams.limit}위까지`} + defaultValue={`${finalParams.limit}위까지`} /> {
- {data?.map(({ name, value }, index) => ( - - ))} + {isEmpty ? ( + + ) : ( + data?.map(({ name, value }, index) => ( + + )) + )}
); diff --git a/src/app/(auth-required)/main/Content.tsx b/src/app/(auth-required)/main/Content.tsx index 778af3d..caac4b9 100644 --- a/src/app/(auth-required)/main/Content.tsx +++ b/src/app/(auth-required)/main/Content.tsx @@ -7,7 +7,7 @@ import { postList, postSummary } from '@/apis'; import { Section, Summary } from '@/app/components'; import { PATHS, SORT_TYPE } from '@/constants'; import { useSearchParam } from '@/hooks'; -import { Button, Dropdown, Check } from '@/shared'; +import { Button, Dropdown, Check, EmptyState } from '@/shared'; import { SortKey, SortValue } from '@/types'; import { convertDateToKST } from '@/utils'; @@ -21,7 +21,11 @@ export const Content = () => { const { ref, inView } = useInView(); - const { data: posts, fetchNextPage } = useInfiniteQuery({ + const { + data: posts, + fetchNextPage, + isLoading, + } = useInfiniteQuery({ queryKey: [PATHS.POSTS, [searchParams.asc, searchParams.sort]], queryFn: async ({ pageParam = '' }) => await postList( @@ -50,6 +54,9 @@ export const Content = () => { const joinedPosts = useMemo(() => posts?.pages.flatMap((i) => i.posts), [posts]); + // 로딩 중이 아니고 게시물이 없는 경우 + const isEmpty = !isLoading && (!joinedPosts || joinedPosts.length === 0); + return (
{summaries && } @@ -91,9 +98,17 @@ export const Content = () => {
- {joinedPosts?.map((item, index, array) => ( -
- ))} + {isEmpty ? ( + + ) : ( + joinedPosts?.map((item, index, array) => ( +
+ )) + )}
diff --git a/src/app/components/Header/index.tsx b/src/app/components/Header/index.tsx index c71d498..f0f1589 100644 --- a/src/app/components/Header/index.tsx +++ b/src/app/components/Header/index.tsx @@ -52,9 +52,9 @@ export const Header = () => { const { data: profiles } = useQuery({ queryKey: [PATHS.ME], queryFn: me, - enabled: !!client.getQueryData([PATHS.ME]), - // 로그아웃 후 리렌더링되어 다시 fetch되는 경우 해결 - // 어차피 prefetch를 통해 데이터를 불러온 상태에서 렌더하기 때문에, 캐시 여부만 판단하면 됨 + staleTime: 1000 * 60 * 5, // 5분간 캐시 유지 + retry: 1, + // 테스트 환경에서도 동작하도록 enabled 조건 완화 }); useEffect(() => { diff --git a/src/constants/env.constant.ts b/src/constants/env.constant.ts index 80e0624..a69f171 100644 --- a/src/constants/env.constant.ts +++ b/src/constants/env.constant.ts @@ -10,7 +10,8 @@ export const ENVS = (() => { SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, } as const; - if (env.NODE_ENV) { + // 테스트 환경이나 브라우저 환경에서는 환경변수 검사 건너뛰기 + if (env.NODE_ENV && typeof window === 'undefined' && !process.env.CYPRESS) { Object.entries(env).forEach(([key, value]) => { if (!value) throw new EnvNotFoundError(key); }); diff --git a/src/shared/EmptyState.tsx b/src/shared/EmptyState.tsx new file mode 100644 index 0000000..9ef6224 --- /dev/null +++ b/src/shared/EmptyState.tsx @@ -0,0 +1,17 @@ +interface EmptyStateProps { + title: string; + description?: string; + icon?: React.ReactNode; +} + +export const EmptyState = ({ title, description, icon }: EmptyStateProps) => { + return ( +
+ {icon &&
{icon}
} +
+

{title}

+ {description &&

{description}

} +
+
+ ); +}; diff --git a/src/shared/index.ts b/src/shared/index.ts index a8fb088..48a9825 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -5,3 +5,4 @@ export * from './Input'; export * from './Check'; export * from './Modal'; export * from './Icon'; +export * from './EmptyState'; From f4576a06287af80b71195b68f0329051902ed65d Mon Sep 17 00:00:00 2001 From: six-standard Date: Sun, 13 Jul 2025 20:36:19 +0900 Subject: [PATCH 07/14] =?UTF-8?q?refactor:=20ga=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EA=B0=92=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cypress.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress.config.ts b/cypress.config.ts index c5b5b40..c90cf30 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -15,10 +15,11 @@ export default defineConfig({ env: { NEXT_PUBLIC_BASE_URL: 'http://localhost:3000', NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY: 'test_key', - NEXT_PUBLIC_GA_ID: 'test_ga_id', + NEXT_PUBLIC_GA_ID: '', NEXT_PUBLIC_SENTRY_AUTH_TOKEN: 'test_sentry_token', NEXT_PUBLIC_SENTRY_DSN: 'test_sentry_dsn', }, + /* eslint-disable @typescript-eslint/no-unused-vars */ setupNodeEvents(_on, _config) { // implement node event listeners here }, From a7beb4f8ce658072eea465560fba1f4e2a816202 Mon Sep 17 00:00:00 2001 From: six-standard Date: Sun, 13 Jul 2025 22:20:30 +0900 Subject: [PATCH 08/14] =?UTF-8?q?refactor:=20=EC=95=88=20=EC=93=B0?= =?UTF-8?q?=EC=9D=B4=EB=8A=94=20msw=20=EA=B4=80=EB=A0=A8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README_TESTING.md | 219 ------------------ package.json | 24 +- pnpm-lock.yaml | 423 ---------------------------------- public/mockServiceWorker.js | 307 ------------------------ setupTests.ts | 10 - src/__mock__/browser.ts | 28 --- src/__mock__/handlers.ts | 448 ------------------------------------ src/__mock__/responses.ts | 58 ----- src/__mock__/server.ts | 38 --- src/app/msw-provider.tsx | 34 --- 10 files changed, 7 insertions(+), 1582 deletions(-) delete mode 100644 README_TESTING.md delete mode 100644 public/mockServiceWorker.js delete mode 100644 setupTests.ts delete mode 100644 src/__mock__/browser.ts delete mode 100644 src/__mock__/handlers.ts delete mode 100644 src/__mock__/responses.ts delete mode 100644 src/__mock__/server.ts delete mode 100644 src/app/msw-provider.tsx diff --git a/README_TESTING.md b/README_TESTING.md deleted file mode 100644 index 635200f..0000000 --- a/README_TESTING.md +++ /dev/null @@ -1,219 +0,0 @@ -# 테스트 설정 가이드 - -## 🚀 MSW + Cypress 테스트 환경 구성 - -이 프로젝트는 **MSW(Mock Service Worker)**와 **Cypress**를 활용한 E2E 테스트 환경을 구성하였습니다. - -### 📋 주요 특징 - -- **API 프로젝트 응답 형식에 맞춘 MSW 모킹** -- **로그인 건너뛰기 기능 (사전 인증 토큰 설정)** -- **모든 API 엔드포인트에 대한 완전한 모킹** -- **실제 네트워크 요청 없이 빠른 테스트 실행** - -### 🔧 설치된 패키지 - -```bash -# MSW 관련 -msw@^2.7.3 - -# Cypress 관련 -cypress@^14.5.1 -@cypress/webpack-preprocessor@^6.0.4 -start-server-and-test@^2.0.12 -``` - -### 🏗️ 프로젝트 구조 - -``` -WEB/ -├── src/ -│ └── __mock__/ -│ ├── handlers.ts # MSW 핸들러 (모든 API 엔드포인트) -│ ├── responses.ts # 응답 헬퍼 함수 -│ ├── server.ts # Node.js 환경용 MSW 서버 -│ └── browser.ts # 브라우저 환경용 MSW 워커 -├── cypress/ -│ ├── e2e/ -│ │ ├── login.cy.ts # 로그인 페이지 테스트 -│ │ ├── main.cy.ts # 메인 페이지 테스트 -│ │ └── leaderboards.cy.ts # 리더보드 페이지 테스트 -│ └── support/ -│ ├── e2e.ts # Cypress 지원 파일 -│ └── commands.ts # 커스텀 명령어 -├── cypress.config.ts # Cypress 설정 -└── jest.setup.ts # Jest + MSW 설정 -``` - -### 📝 MSW 핸들러 구성 - -모든 API 엔드포인트에 대한 MSW 핸들러가 구성되어 있습니다: - -#### 사용자 관련 API - -- `POST /api/login` - 사용자 로그인 -- `POST /api/login-sample` - 샘플 로그인 -- `POST /api/logout` - 로그아웃 -- `GET /api/me` - 현재 사용자 정보 -- `POST /api/qr-login` - QR 로그인 토큰 생성 -- `GET /api/qr-login` - QR 로그인 토큰 조회 - -#### 게시물 관련 API - -- `GET /api/posts` - 게시물 목록 조회 -- `GET /api/posts-stats` - 게시물 통계 -- `GET /api/post/:postId` - 게시물 상세 조회 (ID 기반) -- `GET /api/post/velog/:postId` - 게시물 상세 조회 (UUID 기반) - -#### 리더보드 관련 API - -- `GET /api/leaderboard/user` - 사용자 리더보드 -- `GET /api/leaderboard/post` - 게시물 리더보드 - -#### 기타 API - -- `GET /api/total-stats` - 전체 통계 -- `GET /api/notis` - 공지사항 -- `POST /api/webhook/sentry` - Sentry 웹훅 - -### 🎯 테스트 실행 방법 - -#### 1. Cypress 테스트 실행 - -```bash -# 개발 환경에서 Cypress 열기 -pnpm e2e:dev - -# 헤드리스 모드로 Cypress 실행 -pnpm e2e:test - -# Cypress만 실행 (서버가 이미 실행 중일 때) -pnpm cypress:open -pnpm cypress:run -``` - -#### 2. Jest 테스트 실행 - -```bash -# 일반 Jest 테스트 -pnpm test -``` - -### 🔐 인증 토큰 관리 - -#### 모킹 토큰 - -```typescript -// src/__mock__/handlers.ts -export const MOCK_ACCESS_TOKEN = 'mock_access_token_12345'; -export const MOCK_REFRESH_TOKEN = 'mock_refresh_token_67890'; -``` - -#### 로그인 건너뛰기 - -로그인 페이지를 제외한 모든 페이지 테스트에서는 `cy.setAuthCookies()` 명령어를 사용하여 사전에 인증 토큰을 설정합니다: - -```typescript -// cypress/e2e/main.cy.ts -describe('메인 페이지', () => { - beforeEach(() => { - // 인증 토큰 설정 (로그인 건너뛰기) - cy.setAuthCookies(); - - // 메인 페이지 방문 - cy.visit('/main'); - }); - - // ... 테스트 코드 -}); -``` - -### 🛠️ 커스텀 Cypress 명령어 - -#### `cy.setAuthCookies()` - -인증 토큰을 쿠키에 설정하여 로그인 상태를 모킹합니다. - -#### `cy.clearAuthCookies()` - -인증 토큰을 쿠키에서 제거합니다. - -#### `cy.waitForPageLoad()` - -페이지 로드가 완료될 때까지 대기합니다. - -### 📊 테스트 범위 - -#### 로그인 페이지 (`login.cy.ts`) - -- ✅ 페이지 로드 확인 -- ✅ 로그인 폼 존재 확인 -- ✅ 유효한 토큰으로 로그인 성공 -- ✅ 유효하지 않은 토큰으로 로그인 실패 -- ✅ 샘플 로그인 기능 - -#### 메인 페이지 (`main.cy.ts`) - -- ✅ 페이지 로드 확인 -- ✅ 대시보드 통계 정보 표시 -- ✅ 게시물 목록 표시 -- ✅ 사이드바 네비게이션 동작 -- ✅ 헤더 정보 표시 -- ✅ 차트 렌더링 -- ✅ 정렬 기능 동작 -- ✅ 페이지네이션 동작 -- ✅ 로그아웃 기능 - -#### 리더보드 페이지 (`leaderboards.cy.ts`) - -- ✅ 페이지 로드 확인 -- ✅ 사용자 리더보드 표시 -- ✅ 게시물 리더보드 표시 -- ✅ 필터 기능 동작 -- ✅ 랭킹 순위 표시 -- ✅ 통계 변화량 표시 -- ✅ 헤더 네비게이션 동작 -- ✅ 프로필 이미지 표시 - -### 🔄 개발 워크플로우 - -1. **개발 서버 실행** - - ```bash - pnpm dev - ``` - -2. **테스트 실행** - - ```bash - # 새 터미널에서 - pnpm e2e:dev - ``` - -3. **테스트 작성** - - `cypress/e2e/` 폴더에 `*.cy.ts` 파일 생성 - - 필요한 경우 `src/__mock__/handlers.ts`에 새로운 API 핸들러 추가 - -### 🚨 주의사항 - -1. **실제 API 호출 금지**: 모든 API 요청은 MSW를 통해 모킹됩니다. -2. **토큰 관리**: 로그인 테스트 외에는 반드시 `cy.setAuthCookies()`를 사용하세요. -3. **선택자 전략**: 가능한 경우 `data-testid` 속성을 사용하고, 없는 경우 텍스트 기반 선택자를 사용합니다. - -### 📈 성능 최적화 - -- MSW를 통한 네트워크 요청 모킹으로 빠른 테스트 실행 -- 로그인 건너뛰기를 통한 테스트 시간 단축 -- 병렬 테스트 실행 지원 - -### 🎉 완료된 설정 - -✅ 기존 MSW 코드 제거 -✅ API 프로젝트 응답 형식에 맞춘 새로운 MSW 구성 -✅ 모든 API 엔드포인트에 대한 MSW 핸들러 생성 -✅ Cypress 설정 및 기본 구성 -✅ 각 페이지별 Cypress 테스트 코드 생성 -✅ 로그인 건너뛰기 기능 구현 -✅ Jest + MSW 통합 설정 - -이제 안정적이고 빠른 E2E 테스트를 실행할 수 있습니다! 🚀 diff --git a/package.json b/package.json index ce016ea..ec050d4 100644 --- a/package.json +++ b/package.json @@ -11,17 +11,15 @@ "scripts": { "dev": "next dev --port 3000", "build": "next build", - "autoBuild": "next build && cp -R public .next/standalone && mv .next/static .next/standalone/.next", "start": "next start", - "lint": "next lint", - "lintTest": "eslint ./src/__test__", - "format": "prettier --check --ignore-path .gitignore --ignore-path pnpm-lock.yaml .", - "test": "jest", + "autoBuild": "next build && cp -R public .next/standalone && mv .next/static .next/standalone/.next", + "eslint:lint": "next lint", + "eslint:lintTest": "eslint ./src/__test__", + "prettier:format": "prettier --check --ignore-path .gitignore --ignore-path pnpm-lock.yaml .", + "jest:test": "jest", "cypress:open": "cypress open", "cypress:run": "cypress run", - "cypress:test": "cypress run --headless", - "e2e:dev": "start-server-and-test dev http://localhost:3000 cypress:open", - "e2e:test": "start-server-and-test dev http://localhost:3000 cypress:test" + "cypress:test": "cypress run --headless" }, "dependencies": { "@channel.io/channel-web-sdk-loader": "^2.0.0", @@ -81,21 +79,13 @@ "husky": "^9.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "jest-fetch-mock": "^3.0.3", "jest-fixed-jsdom": "^0.0.9", "lint-staged": "^15.5.0", - "msw": "^2.7.3", "postcss": "^8", "prettier": "^3.3.3", - "start-server-and-test": "^2.0.12", "tailwindcss": "^3.4.1", "ts-node": "^10.9.2", "typescript": "^5", "typescript-eslint": "^8.15.0" - }, - "msw": { - "workerDirectory": [ - "public" - ] } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d541c8..0d24524 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,27 +174,18 @@ importers: jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 - jest-fetch-mock: - specifier: ^3.0.3 - version: 3.0.3 jest-fixed-jsdom: specifier: ^0.0.9 version: 0.0.9(jest-environment-jsdom@29.7.0) lint-staged: specifier: ^15.5.0 version: 15.5.0 - msw: - specifier: ^2.7.3 - version: 2.7.3(@types/node@20.17.10)(typescript@5.7.2) postcss: specifier: ^8 version: 8.4.49 prettier: specifier: ^3.3.3 version: 3.4.2 - start-server-and-test: - specifier: ^2.0.12 - version: 2.0.12 tailwindcss: specifier: ^3.4.1 version: 3.4.17(ts-node@10.9.2(@types/node@20.17.10)(typescript@5.7.2)) @@ -842,15 +833,6 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@bundled-es-modules/cookie@2.0.1': - resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} - - '@bundled-es-modules/statuses@1.0.1': - resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} - - '@bundled-es-modules/tough-cookie@0.1.6': - resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} - '@channel.io/channel-web-sdk-loader@2.0.0': resolution: {integrity: sha512-Z8DDpf2lAaYr/3aAnwQtxg0L8MYWgi/hhGV8c5/SLCV6Fx5Gssj7mfyHHrCVC315B0icmLYqZsXBlmbf6cN8Jg==} @@ -908,12 +890,6 @@ packages: resolution: {integrity: sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@hapi/hoek@9.3.0': - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - - '@hapi/topo@5.1.0': - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1037,37 +1013,6 @@ packages: cpu: [x64] os: [win32] - '@inquirer/confirm@5.1.8': - resolution: {integrity: sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/core@10.1.9': - resolution: {integrity: sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/figures@1.0.11': - resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} - engines: {node: '>=18'} - - '@inquirer/type@3.0.5': - resolution: {integrity: sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1173,10 +1118,6 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@mswjs/interceptors@0.37.6': - resolution: {integrity: sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==} - engines: {node: '>=18'} - '@next/env@14.2.18': resolution: {integrity: sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==} @@ -1262,15 +1203,6 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} - '@open-draft/deferred-promise@2.2.0': - resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} - - '@open-draft/logger@0.3.0': - resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} - - '@open-draft/until@2.1.0': - resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@opentelemetry/api-logs@0.53.0': resolution: {integrity: sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==} engines: {node: '>=14'} @@ -1696,15 +1628,6 @@ packages: peerDependencies: webpack: '>=4.40.0' - '@sideway/address@4.1.5': - resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} - - '@sideway/formula@3.0.1': - resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} - - '@sideway/pinpoint@2.0.0': - resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -1887,9 +1810,6 @@ packages: '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/eslint-config-prettier@6.11.3': resolution: {integrity: sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==} @@ -1970,9 +1890,6 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/statuses@2.0.5': - resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} - '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -2384,9 +2301,6 @@ packages: resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} - axios@1.10.0: - resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} - axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -2610,10 +2524,6 @@ packages: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} - cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} - client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -2689,10 +2599,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - core-js-compat@3.39.0: resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} @@ -2716,9 +2622,6 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-fetch@3.2.0: - resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2822,15 +2725,6 @@ packages: supports-color: optional: true - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} @@ -2944,9 +2838,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -3230,9 +3121,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - event-stream@3.3.4: - resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} - eventemitter2@6.4.7: resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} @@ -3346,15 +3234,6 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -3372,9 +3251,6 @@ packages: forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} - from@0.1.7: - resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} - fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -3508,10 +3384,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql@16.10.0: - resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -3543,9 +3415,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - headers-polyfill@4.0.3: - resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} - hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -3721,9 +3590,6 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} - is-node-process@1.2.0: - resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} - is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -3892,9 +3758,6 @@ packages: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-fetch-mock@3.0.3: - resolution: {integrity: sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==} - jest-fixed-jsdom@0.0.9: resolution: {integrity: sha512-KPfqh2+sn5q2B+7LZktwDcwhCpOpUSue8a1I+BcixWLOQoEVyAjAGfH+IYZGoxZsziNojoHGRTC8xRbB1wDD4g==} engines: {node: '>=18.0.0'} @@ -3992,9 +3855,6 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - joi@17.13.3: - resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4196,9 +4056,6 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - map-stream@0.1.0: - resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4275,20 +4132,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msw@2.7.3: - resolution: {integrity: sha512-+mycXv8l2fEAjFZ5sjrtjJDmm2ceKGjrNbBr1durRg6VkU9fNUE/gsmQ51hWbHqs+l35W1iM+ZsmOD9Fd6lspw==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - typescript: '>= 4.8.x' - peerDependenciesMeta: - typescript: - optional: true - - mute-stream@2.0.0: - resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} - engines: {node: ^18.17.0 || >=20.5.0} - mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -4415,9 +4258,6 @@ packages: ospath@1.2.2: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} - outvariant@1.4.3: - resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -4479,16 +4319,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pause-stream@0.0.11: - resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} - pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -4636,9 +4470,6 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} - promise-polyfill@8.3.0: - resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==} - prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -4652,11 +4483,6 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - ps-tree@1.2.0: - resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} - engines: {node: '>= 0.10'} - hasBin: true - psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -4999,9 +4825,6 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - split@0.3.3: - resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -5025,25 +4848,10 @@ packages: resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} engines: {node: '>=6'} - start-server-and-test@2.0.12: - resolution: {integrity: sha512-U6QiS5qsz+DN5RfJJrkAXdooxMDnLZ+n5nR8kaX//ZH19SilF6b58Z3zM9zTfrNIkJepzauHo4RceSgvgUSX9w==} - engines: {node: '>=16'} - hasBin: true - - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - - stream-combiner@0.0.4: - resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} - streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - strict-event-emitter@0.5.1: - resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} - string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -5336,10 +5144,6 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - type-fest@4.37.0: - resolution: {integrity: sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==} - engines: {node: '>=16'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5444,11 +5248,6 @@ packages: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} - wait-on@8.0.3: - resolution: {integrity: sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==} - engines: {node: '>=12.0.0'} - hasBin: true - walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -5602,10 +5401,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors-cjs@2.1.2: - resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} - engines: {node: '>=18'} - zustand@5.0.3: resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} engines: {node: '>=12.20.0'} @@ -6424,19 +6219,6 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@bundled-es-modules/cookie@2.0.1': - dependencies: - cookie: 0.7.2 - - '@bundled-es-modules/statuses@1.0.1': - dependencies: - statuses: 2.0.1 - - '@bundled-es-modules/tough-cookie@0.1.6': - dependencies: - '@types/tough-cookie': 4.0.5 - tough-cookie: 4.1.4 - '@channel.io/channel-web-sdk-loader@2.0.0': {} '@cspotcode/source-map-support@0.8.1': @@ -6533,12 +6315,6 @@ snapshots: '@eslint/js@9.17.0': {} - '@hapi/hoek@9.3.0': {} - - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.0 - '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -6629,32 +6405,6 @@ snapshots: '@img/sharp-win32-x64@0.34.1': optional: true - '@inquirer/confirm@5.1.8(@types/node@20.17.10)': - dependencies: - '@inquirer/core': 10.1.9(@types/node@20.17.10) - '@inquirer/type': 3.0.5(@types/node@20.17.10) - optionalDependencies: - '@types/node': 20.17.10 - - '@inquirer/core@10.1.9(@types/node@20.17.10)': - dependencies: - '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5(@types/node@20.17.10) - ansi-escapes: 4.3.2 - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 20.17.10 - - '@inquirer/figures@1.0.11': {} - - '@inquirer/type@3.0.5(@types/node@20.17.10)': - optionalDependencies: - '@types/node': 20.17.10 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -6865,15 +6615,6 @@ snapshots: '@kurkle/color@0.3.4': {} - '@mswjs/interceptors@0.37.6': - dependencies: - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/logger': 0.3.0 - '@open-draft/until': 2.1.0 - is-node-process: 1.2.0 - outvariant: 1.4.3 - strict-event-emitter: 0.5.1 - '@next/env@14.2.18': {} '@next/eslint-plugin-next@14.2.18': @@ -6931,15 +6672,6 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@open-draft/deferred-promise@2.2.0': {} - - '@open-draft/logger@0.3.0': - dependencies: - is-node-process: 1.2.0 - outvariant: 1.4.3 - - '@open-draft/until@2.1.0': {} - '@opentelemetry/api-logs@0.53.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -7504,14 +7236,6 @@ snapshots: - encoding - supports-color - '@sideway/address@4.1.5': - dependencies: - '@hapi/hoek': 9.3.0 - - '@sideway/formula@3.0.1': {} - - '@sideway/pinpoint@2.0.0': {} - '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -7719,8 +7443,6 @@ snapshots: dependencies: '@types/node': 20.17.10 - '@types/cookie@0.6.0': {} - '@types/eslint-config-prettier@6.11.3': {} '@types/eslint-scope@3.7.7': @@ -7807,8 +7529,6 @@ snapshots: '@types/stack-utils@2.0.3': {} - '@types/statuses@2.0.5': {} - '@types/tedious@4.0.14': dependencies: '@types/node': 20.17.10 @@ -8342,14 +8062,6 @@ snapshots: axe-core@4.10.2: {} - axios@1.10.0(debug@4.4.1): - dependencies: - follow-redirects: 1.15.9(debug@4.4.1) - form-data: 4.0.1 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axobject-query@4.1.0: {} babel-jest@29.7.0(@babel/core@7.26.0): @@ -8602,8 +8314,6 @@ snapshots: slice-ansi: 5.0.0 string-width: 7.2.0 - cli-width@4.1.0: {} - client-only@0.0.1: {} cliui@8.0.1: @@ -8661,8 +8371,6 @@ snapshots: convert-source-map@2.0.0: {} - cookie@0.7.2: {} - core-js-compat@3.39.0: dependencies: browserslist: 4.24.3 @@ -8695,12 +8403,6 @@ snapshots: create-require@1.1.1: {} - cross-fetch@3.2.0: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -8853,10 +8555,6 @@ snapshots: optionalDependencies: supports-color: 8.1.1 - debug@4.4.1: - dependencies: - ms: 2.1.3 - decimal.js@10.4.3: {} dedent@1.5.3: {} @@ -8960,8 +8658,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - duplexer@0.1.2: {} - eastasianwidth@0.2.0: {} ecc-jsbn@0.1.2: @@ -9371,16 +9067,6 @@ snapshots: esutils@2.0.3: {} - event-stream@3.3.4: - dependencies: - duplexer: 0.1.2 - from: 0.1.7 - map-stream: 0.1.0 - pause-stream: 0.0.11 - split: 0.3.3 - stream-combiner: 0.0.4 - through: 2.3.8 - eventemitter2@6.4.7: {} eventemitter3@5.0.1: {} @@ -9523,10 +9209,6 @@ snapshots: flatted@3.3.2: {} - follow-redirects@1.15.9(debug@4.4.1): - optionalDependencies: - debug: 4.4.1 - for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -9546,8 +9228,6 @@ snapshots: forwarded-parse@2.1.2: {} - from@0.1.7: {} - fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 @@ -9697,8 +9377,6 @@ snapshots: graphemer@1.4.0: {} - graphql@16.10.0: {} - has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -9726,8 +9404,6 @@ snapshots: dependencies: function-bind: 1.1.2 - headers-polyfill@4.0.3: {} - hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -9894,8 +9570,6 @@ snapshots: is-map@2.0.3: {} - is-node-process@1.2.0: {} - is-number-object@1.1.1: dependencies: call-bound: 1.0.3 @@ -10151,13 +9825,6 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-fetch-mock@3.0.3: - dependencies: - cross-fetch: 3.2.0 - promise-polyfill: 8.3.0 - transitivePeerDependencies: - - encoding - jest-fixed-jsdom@0.0.9(jest-environment-jsdom@29.7.0): dependencies: jest-environment-jsdom: 29.7.0 @@ -10369,14 +10036,6 @@ snapshots: jiti@1.21.7: {} - joi@17.13.3: - dependencies: - '@hapi/hoek': 9.3.0 - '@hapi/topo': 5.1.0 - '@sideway/address': 4.1.5 - '@sideway/formula': 3.0.1 - '@sideway/pinpoint': 2.0.0 - js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -10605,8 +10264,6 @@ snapshots: dependencies: tmpl: 1.0.5 - map-stream@0.1.0: {} - math-intrinsics@1.1.0: {} mdn-data@2.0.14: {} @@ -10660,33 +10317,6 @@ snapshots: ms@2.1.3: {} - msw@2.7.3(@types/node@20.17.10)(typescript@5.7.2): - dependencies: - '@bundled-es-modules/cookie': 2.0.1 - '@bundled-es-modules/statuses': 1.0.1 - '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.8(@types/node@20.17.10) - '@mswjs/interceptors': 0.37.6 - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/until': 2.1.0 - '@types/cookie': 0.6.0 - '@types/statuses': 2.0.5 - graphql: 16.10.0 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - strict-event-emitter: 0.5.1 - type-fest: 4.37.0 - yargs: 17.7.2 - optionalDependencies: - typescript: 5.7.2 - transitivePeerDependencies: - - '@types/node' - - mute-stream@2.0.0: {} - mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -10824,8 +10454,6 @@ snapshots: ospath@1.2.2: {} - outvariant@1.4.3: {} - p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -10880,14 +10508,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@6.3.0: {} - path-type@4.0.0: {} - pause-stream@0.0.11: - dependencies: - through: 2.3.8 - pend@1.2.0: {} performance-now@2.1.0: {} @@ -11007,8 +10629,6 @@ snapshots: progress@2.0.3: {} - promise-polyfill@8.3.0: {} - prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -11024,10 +10644,6 @@ snapshots: proxy-from-env@1.1.0: {} - ps-tree@1.2.0: - dependencies: - event-stream: 3.3.4 - psl@1.15.0: dependencies: punycode: 2.3.1 @@ -11415,10 +11031,6 @@ snapshots: source-map@0.6.1: {} - split@0.3.3: - dependencies: - through: 2.3.8 - sprintf-js@1.0.3: {} sshpk@1.18.0: @@ -11445,29 +11057,8 @@ snapshots: dependencies: type-fest: 0.7.1 - start-server-and-test@2.0.12: - dependencies: - arg: 5.0.2 - bluebird: 3.7.2 - check-more-types: 2.24.0 - debug: 4.4.1 - execa: 5.1.1 - lazy-ass: 1.6.0 - ps-tree: 1.2.0 - wait-on: 8.0.3(debug@4.4.1) - transitivePeerDependencies: - - supports-color - - statuses@2.0.1: {} - - stream-combiner@0.0.4: - dependencies: - duplexer: 0.1.2 - streamsearch@1.1.0: {} - strict-event-emitter@0.5.1: {} - string-argv@0.3.2: {} string-length@4.0.2: @@ -11787,8 +11378,6 @@ snapshots: type-fest@0.8.1: {} - type-fest@4.37.0: {} - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.3 @@ -11906,16 +11495,6 @@ snapshots: dependencies: xml-name-validator: 4.0.0 - wait-on@8.0.3(debug@4.4.1): - dependencies: - axios: 1.10.0(debug@4.4.1) - joi: 17.13.3 - lodash: 4.17.21 - minimist: 1.2.8 - rxjs: 7.8.2 - transitivePeerDependencies: - - debug - walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -12093,8 +11672,6 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors-cjs@2.1.2: {} - zustand@5.0.3(@types/react@18.3.18)(react@18.3.1): optionalDependencies: '@types/react': 18.3.18 diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js deleted file mode 100644 index 34057e8..0000000 --- a/public/mockServiceWorker.js +++ /dev/null @@ -1,307 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ - -/** - * Mock Service Worker. - * @see https://github.com/mswjs/msw - * - Please do NOT modify this file. - * - Please do NOT serve this file on production. - */ - -const PACKAGE_VERSION = '2.7.3' -const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' -const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') -const activeClientIds = new Set() - -self.addEventListener('install', function () { - self.skipWaiting() -}) - -self.addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()) -}) - -self.addEventListener('message', async function (event) { - const clientId = event.source.id - - if (!clientId || !self.clients) { - return - } - - const client = await self.clients.get(clientId) - - if (!client) { - return - } - - const allClients = await self.clients.matchAll({ - type: 'window', - }) - - switch (event.data) { - case 'KEEPALIVE_REQUEST': { - sendToClient(client, { - type: 'KEEPALIVE_RESPONSE', - }) - break - } - - case 'INTEGRITY_CHECK_REQUEST': { - sendToClient(client, { - type: 'INTEGRITY_CHECK_RESPONSE', - payload: { - packageVersion: PACKAGE_VERSION, - checksum: INTEGRITY_CHECKSUM, - }, - }) - break - } - - case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId) - - sendToClient(client, { - type: 'MOCKING_ENABLED', - payload: { - client: { - id: client.id, - frameType: client.frameType, - }, - }, - }) - break - } - - case 'MOCK_DEACTIVATE': { - activeClientIds.delete(clientId) - break - } - - case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId) - - const remainingClients = allClients.filter((client) => { - return client.id !== clientId - }) - - // Unregister itself when there are no more clients - if (remainingClients.length === 0) { - self.registration.unregister() - } - - break - } - } -}) - -self.addEventListener('fetch', function (event) { - const { request } = event - - // Bypass navigation requests. - if (request.mode === 'navigate') { - return - } - - // Opening the DevTools triggers the "only-if-cached" request - // that cannot be handled by the worker. Bypass such requests. - if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { - return - } - - // Bypass all requests when there are no active clients. - // Prevents the self-unregistered worked from handling requests - // after it's been deleted (still remains active until the next reload). - if (activeClientIds.size === 0) { - return - } - - // Generate unique request ID. - const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId)) -}) - -async function handleRequest(event, requestId) { - const client = await resolveMainClient(event) - const response = await getResponse(event, client, requestId) - - // Send back the response clone for the "response:*" life-cycle events. - // Ensure MSW is active and ready to handle the message, otherwise - // this message will pend indefinitely. - if (client && activeClientIds.has(client.id)) { - ;(async function () { - const responseClone = response.clone() - - sendToClient( - client, - { - type: 'RESPONSE', - payload: { - requestId, - isMockedResponse: IS_MOCKED_RESPONSE in response, - type: responseClone.type, - status: responseClone.status, - statusText: responseClone.statusText, - body: responseClone.body, - headers: Object.fromEntries(responseClone.headers.entries()), - }, - }, - [responseClone.body], - ) - })() - } - - return response -} - -// Resolve the main client for the given event. -// Client that issues a request doesn't necessarily equal the client -// that registered the worker. It's with the latter the worker should -// communicate with during the response resolving phase. -async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId) - - if (activeClientIds.has(event.clientId)) { - return client - } - - if (client?.frameType === 'top-level') { - return client - } - - const allClients = await self.clients.matchAll({ - type: 'window', - }) - - return allClients - .filter((client) => { - // Get only those clients that are currently visible. - return client.visibilityState === 'visible' - }) - .find((client) => { - // Find the client ID that's recorded in the - // set of clients that have registered the worker. - return activeClientIds.has(client.id) - }) -} - -async function getResponse(event, client, requestId) { - const { request } = event - - // Clone the request because it might've been already used - // (i.e. its body has been read and sent to the client). - const requestClone = request.clone() - - function passthrough() { - // Cast the request headers to a new Headers instance - // so the headers can be manipulated with. - const headers = new Headers(requestClone.headers) - - // Remove the "accept" header value that marked this request as passthrough. - // This prevents request alteration and also keeps it compliant with the - // user-defined CORS policies. - const acceptHeader = headers.get('accept') - if (acceptHeader) { - const values = acceptHeader.split(',').map((value) => value.trim()) - const filteredValues = values.filter( - (value) => value !== 'msw/passthrough', - ) - - if (filteredValues.length > 0) { - headers.set('accept', filteredValues.join(', ')) - } else { - headers.delete('accept') - } - } - - return fetch(requestClone, { headers }) - } - - // Bypass mocking when the client is not active. - if (!client) { - return passthrough() - } - - // Bypass initial page load requests (i.e. static assets). - // The absence of the immediate/parent client in the map of the active clients - // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet - // and is not ready to handle requests. - if (!activeClientIds.has(client.id)) { - return passthrough() - } - - // Notify the client that a request has been intercepted. - const requestBuffer = await request.arrayBuffer() - const clientMessage = await sendToClient( - client, - { - type: 'REQUEST', - payload: { - id: requestId, - url: request.url, - mode: request.mode, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: requestBuffer, - keepalive: request.keepalive, - }, - }, - [requestBuffer], - ) - - switch (clientMessage.type) { - case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data) - } - - case 'PASSTHROUGH': { - return passthrough() - } - } - - return passthrough() -} - -function sendToClient(client, message, transferrables = []) { - return new Promise((resolve, reject) => { - const channel = new MessageChannel() - - channel.port1.onmessage = (event) => { - if (event.data && event.data.error) { - return reject(event.data.error) - } - - resolve(event.data) - } - - client.postMessage( - message, - [channel.port2].concat(transferrables.filter(Boolean)), - ) - }) -} - -async function respondWithMock(response) { - // Setting response status code to 0 is a no-op. - // However, when responding with a "Response.error()", the produced Response - // instance will have status code set to 0. Since it's not possible to create - // a Response instance with status code 0, handle that use-case separately. - if (response.status === 0) { - return Response.error() - } - - const mockedResponse = new Response(response.body, response) - - Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { - value: true, - enumerable: true, - }) - - return mockedResponse -} diff --git a/setupTests.ts b/setupTests.ts deleted file mode 100644 index bfd4b47..0000000 --- a/setupTests.ts +++ /dev/null @@ -1,10 +0,0 @@ -import server from './src/__mock__/server'; - -// msw를 이용한 서버를 일단 띄워서 listen 해준다. -beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); - -// 하나하나 끝날 때마다 => 초기화 해준다. -afterEach(() => server.resetHandlers()); - -// Jest 테스트가 전부 끝날 때 => close 해준다. -afterAll(() => server.close()); diff --git a/src/__mock__/browser.ts b/src/__mock__/browser.ts deleted file mode 100644 index b4be7a3..0000000 --- a/src/__mock__/browser.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { setupWorker } from 'msw/browser'; -import { handlers } from './handlers'; - -// MSW 워커 설정 -export const worker = setupWorker(...handlers); - -// 브라우저에서 MSW 시작 -export const startWorker = async () => { - if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') { - await worker.start({ - onUnhandledRequest: 'bypass', - }); - } -}; - -// 브라우저에서 MSW 중지 -export const stopWorker = () => { - if (typeof window !== 'undefined') { - worker.stop(); - } -}; - -// 브라우저에서 MSW 리셋 -export const resetWorker = () => { - if (typeof window !== 'undefined') { - worker.resetHandlers(); - } -}; diff --git a/src/__mock__/handlers.ts b/src/__mock__/handlers.ts deleted file mode 100644 index a1fd32a..0000000 --- a/src/__mock__/handlers.ts +++ /dev/null @@ -1,448 +0,0 @@ -import { http, HttpResponse } from 'msw'; -import { PATHS } from '@/constants'; -import { BaseSuccess, BaseError, UnauthorizedError, checkAuthTokens } from './responses'; - -// 테스트 환경에서 BASE_URL 설정 -const BASE_URL = - typeof window !== 'undefined' - ? 'http://localhost:3000/api' // 브라우저 환경 (Cypress) - : (process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000') + '/api'; // Node.js 환경 - -// 모킹용 유효한 토큰 -export const MOCK_ACCESS_TOKEN = 'mock_access_token_12345'; -export const MOCK_REFRESH_TOKEN = 'mock_refresh_token_67890'; - -// 사용자 로그인 -const login = http.post(`${BASE_URL}${PATHS.LOGIN}`, async ({ request }) => { - const body = (await request.json()) as { accessToken: string; refreshToken: string }; - - // 유효하지 않은 토큰 시뮬레이션 - if (body.accessToken === 'invalid_token' || body.refreshToken === 'invalid_token') { - return BaseError(401, '유효하지 않은 토큰입니다.'); - } - - // 성공 응답 - const response = BaseSuccess( - { - id: 1, - username: 'testuser', - profile: { - thumbnail: 'https://example.com/avatar.png', - }, - }, - '로그인에 성공하였습니다.', - ); - - // 쿠키 설정 - response.headers.append( - 'Set-Cookie', - `access_token=${MOCK_ACCESS_TOKEN}; Path=/; HttpOnly; Secure; SameSite=Strict`, - ); - response.headers.append( - 'Set-Cookie', - `refresh_token=${MOCK_REFRESH_TOKEN}; Path=/; HttpOnly; Secure; SameSite=Strict`, - ); - - return response; -}); - -// 샘플 로그인 -const sampleLogin = http.post(`${BASE_URL}${PATHS.SAMPLELOGIN}`, async () => { - const response = BaseSuccess( - { - id: 999, - username: 'sampleuser', - profile: { - thumbnail: 'https://example.com/sample-avatar.png', - }, - }, - '샘플 로그인에 성공하였습니다.', - ); - - response.headers.append( - 'Set-Cookie', - `access_token=${MOCK_ACCESS_TOKEN}; Path=/; HttpOnly; Secure; SameSite=Strict`, - ); - response.headers.append( - 'Set-Cookie', - `refresh_token=${MOCK_REFRESH_TOKEN}; Path=/; HttpOnly; Secure; SameSite=Strict`, - ); - - return response; -}); - -// 로그아웃 -const logout = http.post(`${BASE_URL}${PATHS.LOGOUT}`, async ({ request }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - const response = BaseSuccess({}, '로그아웃에 성공하였습니다.'); - - // 쿠키 삭제 - response.headers.append( - 'Set-Cookie', - `access_token=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0`, - ); - response.headers.append( - 'Set-Cookie', - `refresh_token=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0`, - ); - - return response; -}); - -// 현재 사용자 정보 -const me = http.get(`${BASE_URL}${PATHS.ME}`, async ({ request }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - return BaseSuccess( - { - id: 'user-1', - username: 'testuser', - email: 'test@example.com', - profile: { - thumbnail: '/profile.jpg', - }, - }, - '사용자 정보 조회에 성공하였습니다.', - ); -}); - -// QR 로그인 토큰 생성 -const createQRToken = http.post(`${BASE_URL}${PATHS.QRLOGIN}`, async ({ request }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - return BaseSuccess({ token: 'qr_token_12345' }, 'QR 로그인 토큰이 생성되었습니다.'); -}); - -// QR 로그인 토큰 조회 -const getQRToken = http.get(`${BASE_URL}${PATHS.QRLOGIN}`, async ({ request }) => { - const url = new URL(request.url); - const token = url.searchParams.get('token'); - - if (!token || token !== 'qr_token_12345') { - return BaseError(404, '만료되었거나 존재하지 않는 토큰입니다.'); - } - - return HttpResponse.redirect('/main', 302); -}); - -// 게시물 목록 조회 -const posts = http.get(`${BASE_URL}${PATHS.POSTS}`, async ({ request }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - const url = new URL(request.url); - const cursor = url.searchParams.get('cursor'); - - return BaseSuccess( - { - nextCursor: cursor ? null : '2025-01-09T00:00:00Z,10', - posts: [ - { - id: 1, - title: '테스트 게시물 1', - slug: 'test-post-1', - views: 150, - likes: 25, - yesterdayViews: 10, - yesterdayLikes: 5, - createdAt: '2025-01-08T10:00:00Z', - releasedAt: '2025-01-08T10:00:00Z', - }, - { - id: 2, - title: '테스트 게시물 2', - slug: 'test-post-2', - views: 200, - likes: 35, - yesterdayViews: 15, - yesterdayLikes: 8, - createdAt: '2025-01-07T15:30:00Z', - releasedAt: '2025-01-07T15:30:00Z', - }, - ], - }, - '게시물 목록 조회에 성공하였습니다.', - ); -}); - -// 게시물 통계 -const postsStats = http.get(`${BASE_URL}${PATHS.SUMMARY}`, async ({ request }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - return BaseSuccess( - { - totalPostCount: 15, - stats: { - lastUpdatedDate: '2025-01-09T00:00:00Z', - totalLikes: 350, - totalViews: 2500, - yesterdayLikes: 45, - yesterdayViews: 180, - }, - }, - '게시물 통계 조회에 성공하였습니다.', - ); -}); - -// 게시물 상세 조회 (ID 기반) -const postDetail = http.get(`${BASE_URL}${PATHS.DETAIL}/:postId`, async ({ request, params }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - const postId = params.postId as string; - - return BaseSuccess( - { - post: { - id: parseInt(postId), - title: `테스트 게시물 ${postId}`, - slug: `test-post-${postId}`, - views: 250, - likes: 40, - yesterdayViews: 20, - yesterdayLikes: 8, - createdAt: '2025-01-08T10:00:00Z', - releasedAt: '2025-01-08T10:00:00Z', - stats: [ - { date: '2025-01-07T00:00:00Z', views: 100, likes: 15 }, - { date: '2025-01-08T00:00:00Z', views: 150, likes: 25 }, - ], - }, - }, - '게시물 상세 조회에 성공하였습니다.', - ); -}); - -// 게시물 상세 조회 (UUID 기반) -const postByUUID = http.get( - `${BASE_URL}${PATHS.DETAIL}/velog/:postId`, - async ({ request, params }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - const postId = params.postId as string; - - return BaseSuccess( - { - post: { - id: postId, - title: `UUID 기반 테스트 게시물`, - slug: `uuid-test-post`, - views: 300, - likes: 50, - yesterdayViews: 25, - yesterdayLikes: 10, - createdAt: '2025-01-08T10:00:00Z', - releasedAt: '2025-01-08T10:00:00Z', - stats: [ - { date: '2025-01-07T00:00:00Z', views: 150, likes: 25 }, - { date: '2025-01-08T00:00:00Z', views: 175, likes: 35 }, - ], - }, - }, - 'UUID 기반 게시물 상세 조회에 성공하였습니다.', - ); - }, -); - -// 사용자 리더보드 -const userLeaderboard = http.get(`${BASE_URL}${PATHS.LEADERBOARD}/user`, async ({ request }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - return BaseSuccess( - { - users: [ - { - id: 'user-1', - email: 'user1@example.com', - username: 'topuser1', - totalViews: 15000, - totalLikes: 1200, - totalPosts: 45, - viewDiff: 500, - likeDiff: 50, - postDiff: 3, - }, - { - id: 'user-2', - email: 'user2@example.com', - username: 'topuser2', - totalViews: 12000, - totalLikes: 980, - totalPosts: 38, - viewDiff: 300, - likeDiff: 40, - postDiff: 2, - }, - { - id: 'user-3', - email: 'user3@example.com', - username: 'topuser3', - totalViews: 10000, - totalLikes: 800, - totalPosts: 30, - viewDiff: 250, - likeDiff: 35, - postDiff: 1, - }, - ], - }, - '사용자 리더보드 조회에 성공하였습니다.', - ); -}); - -// 게시물 리더보드 -const postLeaderboard = http.get(`${BASE_URL}${PATHS.LEADERBOARD}/post`, async ({ request }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - return BaseSuccess( - { - posts: [ - { - id: 'post-1', - title: '인기 게시물 1', - slug: 'popular-post-1', - username: 'author1', - totalViews: 5000, - totalLikes: 400, - viewDiff: 200, - likeDiff: 30, - releasedAt: '2025-01-07T10:00:00Z', - }, - { - id: 'post-2', - title: '인기 게시물 2', - slug: 'popular-post-2', - username: 'author2', - totalViews: 4500, - totalLikes: 350, - viewDiff: 150, - likeDiff: 25, - releasedAt: '2025-01-06T14:30:00Z', - }, - { - id: 'post-3', - title: '인기 게시물 3', - slug: 'popular-post-3', - username: 'author3', - totalViews: 4000, - totalLikes: 300, - viewDiff: 120, - likeDiff: 20, - releasedAt: '2025-01-05T09:15:00Z', - }, - ], - }, - '게시물 리더보드 조회에 성공하였습니다.', - ); -}); - -// 전체 통계 -const totalStats = http.get(`${BASE_URL}${PATHS.TOTALSTATS}`, async ({ request }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - const url = new URL(request.url); - const type = url.searchParams.get('type') || 'view'; - - const getMessage = (type: string) => { - switch (type) { - case 'view': - return '전체 조회수 변동 조회에 성공하였습니다.'; - case 'like': - return '전체 좋아요 변동 조회에 성공하였습니다.'; - case 'post': - return '전체 게시글 수 변동 조회에 성공하였습니다.'; - default: - return '전체 통계 조회에 성공하였습니다.'; - } - }; - - return BaseSuccess( - [ - { date: '2025-01-03T00:00:00Z', value: 100 }, - { date: '2025-01-04T00:00:00Z', value: 150 }, - { date: '2025-01-05T00:00:00Z', value: 200 }, - { date: '2025-01-06T00:00:00Z', value: 180 }, - { date: '2025-01-07T00:00:00Z', value: 250 }, - { date: '2025-01-08T00:00:00Z', value: 300 }, - { date: '2025-01-09T00:00:00Z', value: 350 }, - ], - getMessage(type), - ); -}); - -// 공지사항 -const notifications = http.get(`${BASE_URL}${PATHS.NOTIS}`, async ({ request }) => { - if (!checkAuthTokens(request.headers)) { - return UnauthorizedError('인증이 필요합니다.'); - } - - return BaseSuccess( - { - posts: [ - { - id: 'noti-1', - title: '시스템 점검 안내', - content: '시스템 점검이 예정되어 있습니다.', - createdAt: '2025-01-08T09:00:00Z', - isImportant: true, - }, - { - id: 'noti-2', - title: '새로운 기능 업데이트', - content: '새로운 기능이 추가되었습니다.', - createdAt: '2025-01-07T16:00:00Z', - isImportant: false, - }, - ], - }, - '공지사항 조회에 성공하였습니다.', - ); -}); - -// Sentry 웹훅 (테스트용) -const sentryWebhook = http.post(`${BASE_URL}/webhook/sentry`, async ({ request }) => { - const body = (await request.json()) as { action: string; data?: unknown; actor?: unknown }; - - if (body.action !== 'created') { - return BaseError(400, 'Sentry 웹훅 처리에 실패했습니다.'); - } - - return BaseSuccess({}, 'Sentry 웹훅 처리에 성공하였습니다.'); -}); - -export const handlers = [ - login, - sampleLogin, - logout, - me, - createQRToken, - getQRToken, - posts, - postsStats, - postDetail, - postByUUID, - userLeaderboard, - postLeaderboard, - totalStats, - notifications, - sentryWebhook, -]; diff --git a/src/__mock__/responses.ts b/src/__mock__/responses.ts deleted file mode 100644 index 36abeeb..0000000 --- a/src/__mock__/responses.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { HttpResponse } from 'msw'; - -// API 프로젝트의 BaseResponseDto 형식에 맞춘 응답 타입 -export interface BaseResponseDto { - success: boolean; - message: string; - data: T; - error: string | null; -} - -// 성공 응답 -export const BaseSuccess = ( - data: T, - message: string = '성공적으로 처리되었습니다.', -): HttpResponse => - HttpResponse.json>( - { - success: true, - message, - data, - error: null, - }, - { status: 200 }, - ); - -// 에러 응답 -export const BaseError = (statusCode: number, message: string): HttpResponse => - HttpResponse.json>( - { - success: false, - message, - data: null, - error: message, - }, - { - status: statusCode, - statusText: message, - }, - ); - -// 인증 실패 응답 -export const UnauthorizedError = (message: string = '인증이 필요합니다.'): HttpResponse => - BaseError(401, message); - -// 잘못된 요청 응답 -export const BadRequestError = (message: string = '잘못된 요청입니다.'): HttpResponse => - BaseError(400, message); - -// 서버 오류 응답 -export const InternalServerError = ( - message: string = '서버 내부 오류가 발생했습니다.', -): HttpResponse => BaseError(500, message); - -// 인증 토큰 확인 유틸리티 -export const checkAuthTokens = (headers: Headers): boolean => { - const cookies = headers.get('cookie') || ''; - return cookies.includes('access_token') && cookies.includes('refresh_token'); -}; diff --git a/src/__mock__/server.ts b/src/__mock__/server.ts deleted file mode 100644 index 8d43b9c..0000000 --- a/src/__mock__/server.ts +++ /dev/null @@ -1,38 +0,0 @@ -// MSW 서버 설정 (Node.js 환경에서만 사용) -export const createMSWServer = async () => { - if (typeof window !== 'undefined') { - throw new Error('MSW server should only be used in Node.js environment'); - } - - const { setupServer } = await import('msw/node'); - const { handlers } = await import('./handlers'); - - return setupServer(...handlers); -}; - -// Cypress에서 사용할 서버 인스턴스 -let serverInstance: Awaited> | null = null; - -// 테스트 환경에서 서버 시작 -export const startServer = async () => { - if (!serverInstance) { - serverInstance = await createMSWServer(); - } - serverInstance.listen({ - onUnhandledRequest: 'error', - }); -}; - -// 테스트 환경에서 서버 중지 -export const stopServer = () => { - if (serverInstance) { - serverInstance.close(); - } -}; - -// 테스트 환경에서 서버 리셋 -export const resetServer = () => { - if (serverInstance) { - serverInstance.resetHandlers(); - } -}; diff --git a/src/app/msw-provider.tsx b/src/app/msw-provider.tsx deleted file mode 100644 index 341e38e..0000000 --- a/src/app/msw-provider.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; - -interface MSWProviderProps { - children: React.ReactNode; -} - -export default function MSWProvider({ children }: MSWProviderProps) { - const [mswReady, setMswReady] = useState(false); - - useEffect(() => { - const init = async () => { - if (typeof window !== 'undefined') { - if (process.env.NODE_ENV === 'development') { - const { worker } = await import('../__mock__/browser'); - await worker.start({ - onUnhandledRequest: 'bypass', - quiet: true, - }); - } - } - setMswReady(true); - }; - - init(); - }, []); - - if (!mswReady) { - return null; - } - - return <>{children}; -} From 7cf6d6b871d373516db86f6007739f4966753bca Mon Sep 17 00:00:00 2001 From: six-standard Date: Sun, 13 Jul 2025 22:24:48 +0900 Subject: [PATCH 09/14] =?UTF-8?q?refactor:=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?=EB=AA=85=EB=A0=B9=EC=96=B4=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 7 +++---- readme.md | 22 +++++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index ec050d4..9eefd01 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,12 @@ "build": "next build", "start": "next start", "autoBuild": "next build && cp -R public .next/standalone && mv .next/static .next/standalone/.next", - "eslint:lint": "next lint", - "eslint:lintTest": "eslint ./src/__test__", - "prettier:format": "prettier --check --ignore-path .gitignore --ignore-path pnpm-lock.yaml .", "jest:test": "jest", + "eslint:lint": "next lint", "cypress:open": "cypress open", "cypress:run": "cypress run", - "cypress:test": "cypress run --headless" + "cypress:test": "cypress run --headless", + "prettier:format": "prettier --check --ignore-path .gitignore --ignore-path pnpm-lock.yaml ." }, "dependencies": { "@channel.io/channel-web-sdk-loader": "^2.0.0", diff --git a/readme.md b/readme.md index be91506..d75e26c 100644 --- a/readme.md +++ b/readme.md @@ -1,19 +1,22 @@ ![image](https://github.com/user-attachments/assets/e43ab765-f94e-41d8-8f57-bf05100606cd) ## Velog Dashboard -- **📅 진행 기간** 2024. 11 ~ ONGOING + +- **📅 진행 기간** 2024. 11 ~ ONGOING - **💻 서비스 URL** [https://velog-dashboard.kro.kr/](https://velog-dashboard.kro.kr/?utm_source=github&utm_medium=repo) (서비스 체험 가능!) ## INTRO -통계 기능이 부실한 블로그 서비스들을 위한 **블로그 통계 대시보드 서비스**입니다. -현재는 Velog 게시물 통계 조회 기능을 제공하고 있으며, 추후 통계 리더보드 및 타 서비스와의 연동을 계획하고 있습니다. +통계 기능이 부실한 블로그 서비스들을 위한 **블로그 통계 대시보드 서비스**입니다. + +현재는 Velog 게시물 통계 조회 기능을 제공하고 있으며, 추후 통계 리더보드 및 타 서비스와의 연동을 계획하고 있습니다. -현재 **200+**명의 유저들이 서비스를 사용하고 있으며, **20000+**개의 게시물의 통계를 관리하고 있습니다. +현재 **200+**명의 유저들이 서비스를 사용하고 있으며, **20000+**개의 게시물의 통계를 관리하고 있습니다. -또한, 프로젝트 초기부터 배포까지의 [회고록](https://velog.io/@six-standard/series/Velog-Dashboard-%EC%B0%B8%EC%97%AC%EA%B8%B0)을 매주 작성하였습니다. +또한, 프로젝트 초기부터 배포까지의 [회고록](https://velog.io/@six-standard/series/Velog-Dashboard-%EC%B0%B8%EC%97%AC%EA%B8%B0)을 매주 작성하였습니다. ## SETUP DOCS + ### 실행 - `git clone https://github.com/Check-Data-Out/velog-dashboard-v2-fe.git` @@ -24,13 +27,14 @@ ### 린팅 -- `pnpm lint` (lint only pages) -- `pnpm lintTest` (lint only tests) -- `pnpm format` (prettier) +- `pnpm eslint:lint` (lint only pages) +- `pnpm prettier:format` (prettier) ### 테스팅 -- `pnpm test` (test all pages & components) +- `pnpm jest:test` (unit test) +- `pnpm cypress:open` (e2e test, with preview screen) +- `pnpm cypress:test` (e2e test, without preview screen) ### local 에서 docker image 생성, 태깅, 푸시, 테스팅까지 From 95c1da61686a18b83f156944834a70dc7857289d Mon Sep 17 00:00:00 2001 From: six-standard Date: Sun, 13 Jul 2025 22:25:24 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor:=20=EC=9E=90=EC=9E=98=ED=95=9C?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=9D=B4=EC=8A=88=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cypress/e2e/leaderboards.cy.ts | 50 +++++++++------------------------- cypress/e2e/login.cy.ts | 15 ---------- cypress/e2e/main.cy.ts | 46 ++++--------------------------- cypress/support/commands.ts | 30 ++++++-------------- cypress/support/e2e.ts | 35 ++++-------------------- 5 files changed, 33 insertions(+), 143 deletions(-) diff --git a/cypress/e2e/leaderboards.cy.ts b/cypress/e2e/leaderboards.cy.ts index 7ddbec0..b1c59a9 100644 --- a/cypress/e2e/leaderboards.cy.ts +++ b/cypress/e2e/leaderboards.cy.ts @@ -1,13 +1,6 @@ -// *********************************************************** -// 리더보드 페이지 E2E 테스트 -// *********************************************************** - describe('리더보드 페이지', () => { beforeEach(() => { - // 인증 토큰 설정 (로그인 건너뛰기) cy.setAuthCookies(); - - // 리더보드 페이지 방문 cy.visit('/leaderboards'); }); @@ -17,76 +10,62 @@ describe('리더보드 페이지', () => { }); it('사용자 리더보드가 표시되어야 한다', () => { - // 사용자 리더보드 기본 선택 확인 (기본값이 '사용자 기준'이므로) cy.get('select').first().should('have.value', '사용자 기준'); - // 사용자 정보 확인 (이메일 앞부분 표시) cy.contains('user1').should('be.visible'); cy.contains('user2').should('be.visible'); - // 통계 정보 확인 (viewDiff 값들) - cy.contains('500').should('be.visible'); // user1의 viewDiff - cy.contains('300').should('be.visible'); // user2의 viewDiff + cy.contains('500').should('be.visible'); + cy.contains('300').should('be.visible'); }); it('게시물 리더보드가 표시되어야 한다', () => { - // 게시물 리더보드로 전환 cy.get('select').first().select('게시글 기준'); - // 게시물 정보 확인 cy.contains('인기 게시물 1').should('be.visible'); cy.contains('인기 게시물 2').should('be.visible'); - // 통계 정보 확인 (viewDiff 값들) - cy.contains('200').should('be.visible'); // post1의 viewDiff - cy.contains('150').should('be.visible'); // post2의 viewDiff + cy.contains('200').should('be.visible'); + cy.contains('150').should('be.visible'); }); it('필터 기능이 동작해야 한다', () => { - // 4개의 드롭다운 확인 (기준, 정렬, 개수, 기간) cy.get('select').should('have.length', 4); - // 정렬 옵션 변경 테스트 cy.get('select').eq(1).select('좋아요 증가순'); cy.get('select').eq(1).select('조회수 증가순'); - // 개수 제한 변경 테스트 cy.get('select').eq(2).select('30위까지'); cy.get('select').eq(2).select('10위까지'); - // 기간 필터 변경 테스트 cy.get('select').eq(3).select('지난 7일'); cy.get('select').eq(3).select('지난 30일'); }); it('랭킹 순위가 표시되어야 한다', () => { - // 순위 표시 확인 cy.get('[data-testid="rank"], [class*="rank"]').should('be.visible'); - cy.contains('1').should('be.visible'); // 1위 - cy.contains('2').should('be.visible'); // 2위 + cy.contains('1').should('be.visible'); + cy.contains('2').should('be.visible'); }); it('통계 변화량이 표시되어야 한다', () => { - // 변화량 숫자 확인 (기본 사용자 리더보드, 조회수 기준) - cy.contains('500').should('be.visible'); // user1의 viewDiff - cy.contains('300').should('be.visible'); // user2의 viewDiff - cy.contains('250').should('be.visible'); // user3의 viewDiff + cy.contains('500').should('be.visible'); + cy.contains('300').should('be.visible'); + cy.contains('250').should('be.visible'); - // 좋아요 기준으로 변경 cy.get('select').eq(1).select('좋아요 증가순'); - cy.contains('50').should('be.visible'); // user1의 likeDiff - cy.contains('40').should('be.visible'); // user2의 likeDiff + cy.contains('50').should('be.visible'); + cy.contains('40').should('be.visible'); }); it('빈 데이터 상태를 올바르게 처리해야 한다', () => { - // 빈 데이터 응답을 모킹 cy.intercept('GET', '**/api/leaderboard/user*', { statusCode: 200, body: { success: true, message: '사용자 리더보드 조회에 성공하였습니다.', data: { - users: [], // 빈 배열 + users: [], }, error: null, }, @@ -98,20 +77,17 @@ describe('리더보드 페이지', () => { success: true, message: '게시물 리더보드 조회에 성공하였습니다.', data: { - posts: [], // 빈 배열 + posts: [], }, error: null, }, }).as('emptyPostLeaderboardAPI'); - // 페이지 새로고침하여 빈 데이터 모킹 적용 cy.reload(); - // 빈 데이터 상태 메시지 확인 cy.contains('리더보드 데이터가 없습니다').should('be.visible'); cy.contains('현재 설정된 조건에 맞는 사용자 데이터가 없습니다').should('be.visible'); - // 게시물 리더보드로 전환하여 빈 데이터 상태 확인 cy.get('select').first().select('게시글 기준'); cy.contains('현재 설정된 조건에 맞는 게시물 데이터가 없습니다').should('be.visible'); }); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index c96e0e8..03049ba 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -1,10 +1,5 @@ -// *********************************************************** -// 로그인 페이지 E2E 테스트 -// *********************************************************** - describe('로그인 페이지', () => { beforeEach(() => { - // 로그인 페이지 방문 cy.visit('/'); }); @@ -25,7 +20,6 @@ describe('로그인 페이지', () => { }); it('유효한 토큰으로 로그인할 수 있어야 한다', () => { - // 토큰 입력 cy.get('input[name*="accessToken"], input[name*="access"], input[placeholder*="Access"]') .first() .type('valid_access_token'); @@ -33,16 +27,13 @@ describe('로그인 페이지', () => { .first() .type('valid_refresh_token'); - // 로그인 버튼 클릭 cy.get('button[type="submit"], button:contains("로그인")').first().click(); - // 메인 페이지로 리디렉션 확인 cy.url().should('include', '/main'); cy.waitForPageLoad(); }); it('유효하지 않은 토큰으로 로그인 시 에러를 표시해야 한다', () => { - // 유효하지 않은 토큰 입력 cy.get('input[name*="accessToken"], input[name*="access"], input[placeholder*="Access"]') .first() .type('invalid_token'); @@ -50,19 +41,13 @@ describe('로그인 페이지', () => { .first() .type('invalid_token'); - // 로그인 버튼 클릭 cy.get('button[type="submit"], button:contains("로그인")').first().click(); - // 401 에러 시 홈페이지로 리디렉트되는 것이 정상 동작 - // (instance.request.ts에서 401 에러 시 자동으로 '/'로 리디렉트) cy.url().should('eq', Cypress.config().baseUrl + '/'); }); it('샘플 로그인 버튼이 동작해야 한다', () => { - // 체험 계정 로그인 버튼 클릭 (실제 텍스트 사용) cy.contains('체험 계정 로그인').should('be.visible').click(); - - // 메인 페이지로 리디렉션 확인 cy.url().should('include', '/main'); cy.waitForPageLoad(); }); diff --git a/cypress/e2e/main.cy.ts b/cypress/e2e/main.cy.ts index 6fa2119..2635191 100644 --- a/cypress/e2e/main.cy.ts +++ b/cypress/e2e/main.cy.ts @@ -1,13 +1,6 @@ -// *********************************************************** -// 메인 페이지 E2E 테스트 -// *********************************************************** - describe('메인 페이지', () => { beforeEach(() => { - // 인증 토큰 설정 (로그인 건너뛰기) cy.setAuthCookies(); - - // 메인 페이지 방문 cy.visit('/main'); }); @@ -17,86 +10,59 @@ describe('메인 페이지', () => { }); it('대시보드 통계 정보가 표시되어야 한다', () => { - // 사이드바 통계 확인 cy.contains('전체 조회수').should('be.visible'); cy.contains('전체 좋아요 수').should('be.visible'); cy.contains('총 게시글 수').should('be.visible'); - // 숫자 값 확인 - cy.contains('2500').should('be.visible'); // totalViews - cy.contains('350').should('be.visible'); // totalLikes - cy.contains('15').should('be.visible'); // totalPostCount + cy.contains('2500').should('be.visible'); + cy.contains('350').should('be.visible'); + cy.contains('15').should('be.visible'); }); it('게시물 목록이 표시되어야 한다', () => { - // 게시물 제목 확인 cy.contains('테스트 게시물 1').should('be.visible'); cy.contains('테스트 게시물 2').should('be.visible'); - // 게시물 통계 확인 - cy.contains('150').should('be.visible'); // 조회수 - cy.contains('25').should('be.visible'); // 좋아요 + cy.contains('150').should('be.visible'); + cy.contains('25').should('be.visible'); }); it('정렬 및 필터 기능이 동작해야 한다', () => { - // 드롭다운 메뉴 확인 cy.get('select').should('be.visible'); - - // 오름차순 체크박스 확인 cy.get('input[type="checkbox"]').should('be.visible'); - - // 새로고침 버튼 확인 (비활성화 상태) cy.contains('새로고침').should('be.visible'); cy.contains('새로고침').should('be.disabled'); }); it('마지막 업데이트 시간이 표시되어야 한다', () => { - // 업데이트 시간 텍스트 확인 cy.contains('마지막 업데이트').should('be.visible'); - - // 날짜 형식 확인 (YYYY-MM-DD 형식이어야 함) cy.contains(/\d{4}-\d{2}-\d{2}/).should('be.visible'); }); it('로그아웃 기능이 동작해야 한다', () => { - // 프로필 클릭하여 드롭다운 열기 cy.get('#profile').click(); - - // 로그아웃 버튼 확인 cy.contains('로그아웃').should('be.visible'); - - // 로그아웃 클릭 cy.contains('로그아웃').click(); - - // 로그인 페이지로 리다이렉트 확인 cy.url().should('include', '/'); }); it('빈 데이터 상태를 올바르게 처리해야 한다', () => { - // 빈 게시물 데이터 응답을 모킹 cy.intercept('GET', '**/api/posts*', { statusCode: 200, body: { success: true, message: '게시물 목록 조회에 성공하였습니다.', - data: { - nextCursor: null, - posts: [], // 빈 배열 - }, + data: { nextCursor: null, posts: [] }, error: null, }, }).as('emptyPostsAPI'); - // 페이지 새로고침하여 빈 데이터 모킹 적용 cy.reload(); - // 빈 데이터 상태 메시지 확인 cy.contains('게시물이 없습니다').should('be.visible'); cy.contains('아직 작성된 게시물이 없습니다. 첫 번째 게시물을 작성해보세요!').should( 'be.visible', ); - - // 📝 이모지 아이콘 확인 cy.contains('📝').should('be.visible'); }); }); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 0e38d83..caa2f6b 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,35 +1,23 @@ -// *********************************************************** -// Cypress 커스텀 명령어 -// *********************************************************** +import { MOCK_ACCESS_TOKEN, MOCK_REFRESH_TOKEN } from './mock'; -import { MOCK_ACCESS_TOKEN, MOCK_REFRESH_TOKEN } from '../../src/__mock__/handlers'; +const DEFAULT_OPTION = { + httpOnly: true, + secure: true, + sameSite: 'strict', + path: '/', +} as const; -// 인증 토큰을 쿠키에 설정하여 로그인 상태를 모킹 Cypress.Commands.add('setAuthCookies', () => { - cy.setCookie('access_token', MOCK_ACCESS_TOKEN, { - httpOnly: true, - secure: true, - sameSite: 'strict', - path: '/', - }); - cy.setCookie('refresh_token', MOCK_REFRESH_TOKEN, { - httpOnly: true, - secure: true, - sameSite: 'strict', - path: '/', - }); - - // API 호출 확인을 위한 대기 + cy.setCookie('access_token', MOCK_ACCESS_TOKEN, DEFAULT_OPTION); + cy.setCookie('refresh_token', MOCK_REFRESH_TOKEN, DEFAULT_OPTION); cy.wait(100); }); -// 인증 토큰을 쿠키에서 제거 Cypress.Commands.add('clearAuthCookies', () => { cy.clearCookie('access_token'); cy.clearCookie('refresh_token'); }); -// 페이지 로드를 기다립니다 Cypress.Commands.add('waitForPageLoad', () => { cy.get('body').should('be.visible'); cy.window().should('have.property', 'document'); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 4eeccbc..e764c20 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,13 +1,6 @@ -// *********************************************************** -// Cypress E2E 지원 파일 -// *********************************************************** - -// Cypress 명령어 타입 정의 import './commands'; -// Cypress에서는 cy.intercept를 사용하여 API 모킹 beforeEach(() => { - // 로그인 API 모킹 cy.intercept('POST', '**/api/login', (req) => { const body = req.body; if (body.accessToken === 'invalid_token' || body.refreshToken === 'invalid_token') { @@ -40,7 +33,6 @@ beforeEach(() => { } }).as('loginAPI'); - // 샘플 로그인 API 모킹 cy.intercept('POST', '**/api/login-sample', { statusCode: 200, body: { @@ -58,7 +50,6 @@ beforeEach(() => { }, }).as('sampleLoginAPI'); - // 사용자 정보 API 모킹 cy.intercept('GET', '**/api/me', { statusCode: 200, body: { @@ -76,14 +67,11 @@ beforeEach(() => { }, }).as('meAPI'); - // 게시물 목록 API 모킹 (첫 번째 페이지) cy.intercept('GET', '**/api/posts*', (req) => { - // URL에서 cursor 확인 const url = new URL(req.url); const cursor = url.searchParams.get('cursor'); if (!cursor) { - // 첫 번째 페이지 - nextCursor 포함 req.reply({ statusCode: 200, body: { @@ -120,7 +108,6 @@ beforeEach(() => { }, }); } else { - // 두 번째 페이지 - nextCursor null로 무한 스크롤 종료 req.reply({ statusCode: 200, body: { @@ -159,7 +146,6 @@ beforeEach(() => { } }).as('postsAPI'); - // 게시물 통계 API 모킹 cy.intercept('GET', '**/api/posts-stats', { statusCode: 200, body: { @@ -179,7 +165,6 @@ beforeEach(() => { }, }).as('postsStatsAPI'); - // 사용자 리더보드 API 모킹 cy.intercept('GET', '**/api/leaderboard/user*', { statusCode: 200, body: { @@ -226,7 +211,6 @@ beforeEach(() => { }, }).as('userLeaderboardAPI'); - // 게시물 리더보드 API 모킹 cy.intercept('GET', '**/api/leaderboard/post*', { statusCode: 200, body: { @@ -273,7 +257,6 @@ beforeEach(() => { }, }).as('postLeaderboardAPI'); - // 전체 통계 API 모킹 cy.intercept('GET', '**/api/total-stats*', { statusCode: 200, body: { @@ -292,7 +275,6 @@ beforeEach(() => { }, }).as('totalStatsAPI'); - // 공지사항 API 모킹 cy.intercept('GET', '**/api/notis', { statusCode: 200, body: { @@ -320,7 +302,6 @@ beforeEach(() => { }, }).as('notisAPI'); - // 로그아웃 API 모킹 cy.intercept('POST', '**/api/logout', { statusCode: 200, body: { @@ -331,7 +312,6 @@ beforeEach(() => { }, }).as('logoutAPI'); - // 게시물 상세 차트 API 모킹 (차트 데이터용) cy.intercept('GET', '**/api/post/**', { statusCode: 200, body: { @@ -353,24 +333,19 @@ beforeEach(() => { }).as('postDetailAPI'); }); -// 전역 타입 선언 +/* eslint-disable @typescript-eslint/no-namespace */ declare global { namespace Cypress { interface Chainable { - /** - * 인증 토큰을 쿠키에 설정하여 로그인 상태를 모킹합니다. - */ + // 인증 토큰을 쿠키에 설정하여 로그인 상태를 모킹합니다. setAuthCookies(): Chainable; - /** - * 인증 토큰을 쿠키에서 제거합니다. - */ + // 인증 토큰을 쿠키에서 제거합니다. clearAuthCookies(): Chainable; - /** - * 페이지 로드를 기다립니다. - */ + // 페이지 로드를 기다립니다. waitForPageLoad(): Chainable; } } } +/* eslint-enable @typescript-eslint/no-namespace */ From 6d545f3663b6bb8f8f64da607e760347e6ed69f3 Mon Sep 17 00:00:00 2001 From: six-standard Date: Sun, 13 Jul 2025 23:06:41 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor:=20=EB=B0=98=EB=B3=B5=EB=90=9C?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=B0=8F=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cypress/e2e/leaderboards.cy.ts | 36 ++-- cypress/e2e/login.cy.ts | 4 +- cypress/e2e/main.cy.ts | 16 +- cypress/support/base.ts | 22 ++ cypress/support/e2e.ts | 369 ++++++--------------------------- cypress/support/index.ts | 4 + cypress/support/mock.ts | 249 ++++++++++++++++++++++ 7 files changed, 356 insertions(+), 344 deletions(-) create mode 100644 cypress/support/base.ts create mode 100644 cypress/support/index.ts create mode 100644 cypress/support/mock.ts diff --git a/cypress/e2e/leaderboards.cy.ts b/cypress/e2e/leaderboards.cy.ts index b1c59a9..e5bfbee 100644 --- a/cypress/e2e/leaderboards.cy.ts +++ b/cypress/e2e/leaderboards.cy.ts @@ -1,3 +1,5 @@ +import { BaseSuccess } from '../support'; + describe('리더보드 페이지', () => { beforeEach(() => { cy.setAuthCookies(); @@ -59,29 +61,17 @@ describe('리더보드 페이지', () => { }); it('빈 데이터 상태를 올바르게 처리해야 한다', () => { - cy.intercept('GET', '**/api/leaderboard/user*', { - statusCode: 200, - body: { - success: true, - message: '사용자 리더보드 조회에 성공하였습니다.', - data: { - users: [], - }, - error: null, - }, - }).as('emptyUserLeaderboardAPI'); - - cy.intercept('GET', '**/api/leaderboard/post*', { - statusCode: 200, - body: { - success: true, - message: '게시물 리더보드 조회에 성공하였습니다.', - data: { - posts: [], - }, - error: null, - }, - }).as('emptyPostLeaderboardAPI'); + cy.intercept( + 'GET', + '**/api/leaderboard/user*', + BaseSuccess({ users: [] }, '사용자 리더보드 조회에 성공하였습니다.'), + ).as('emptyUserLeaderboardAPI'); + + cy.intercept( + 'GET', + '**/api/leaderboard/post*', + BaseSuccess({ posts: [] }, '게시물 리더보드 조회에 성공하였습니다.'), + ).as('emptyPostLeaderboardAPI'); cy.reload(); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 03049ba..a78ba67 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -1,7 +1,5 @@ describe('로그인 페이지', () => { - beforeEach(() => { - cy.visit('/'); - }); + beforeEach(() => cy.visit('/')); it('페이지가 정상적으로 로드되어야 한다', () => { cy.waitForPageLoad(); diff --git a/cypress/e2e/main.cy.ts b/cypress/e2e/main.cy.ts index 2635191..bda8d2d 100644 --- a/cypress/e2e/main.cy.ts +++ b/cypress/e2e/main.cy.ts @@ -1,3 +1,5 @@ +import { BaseSuccess } from '../support'; + describe('메인 페이지', () => { beforeEach(() => { cy.setAuthCookies(); @@ -47,15 +49,11 @@ describe('메인 페이지', () => { }); it('빈 데이터 상태를 올바르게 처리해야 한다', () => { - cy.intercept('GET', '**/api/posts*', { - statusCode: 200, - body: { - success: true, - message: '게시물 목록 조회에 성공하였습니다.', - data: { nextCursor: null, posts: [] }, - error: null, - }, - }).as('emptyPostsAPI'); + cy.intercept( + 'GET', + '**/api/posts*', + BaseSuccess({ nextCursor: null, posts: [] }, '게시물 목록 조회에 성공하였습니다.'), + ).as('emptyPostsAPI'); cy.reload(); diff --git a/cypress/support/base.ts b/cypress/support/base.ts new file mode 100644 index 0000000..6dcb89f --- /dev/null +++ b/cypress/support/base.ts @@ -0,0 +1,22 @@ +export const BaseSuccess = (data: T, message: string = '성공적으로 처리되었습니다.') => ({ + statusCode: 200, + body: { + success: true, + message, + data, + error: null, + }, +}); + +export const BaseError = (statusCode: number, message: string) => ({ + statusCode, + body: { + success: false, + message, + data: null, + error: { + name: 'ServerError', + message, + }, + }, +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index e764c20..5f49695 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,336 +1,87 @@ +import { BaseError, BaseSuccess } from './base'; +import { + notificationsResponseData, + postLeaderboardResponseData, + postsFirstData, + postsGraphData, + postsSecondData, + postsStatsResponseData, + totalStatsResponseData, + userLeaderboardResponseData, + userResponseData, +} from './mock'; import './commands'; beforeEach(() => { cy.intercept('POST', '**/api/login', (req) => { const body = req.body; if (body.accessToken === 'invalid_token' || body.refreshToken === 'invalid_token') { - req.reply({ - statusCode: 401, - body: { - success: false, - message: '유효하지 않은 토큰입니다.', - data: null, - error: '유효하지 않은 토큰입니다.', - }, - }); + req.reply(BaseError(401, '유효하지 않은 토큰입니다.')); } else { - req.reply({ - statusCode: 200, - body: { - success: true, - message: '로그인에 성공하였습니다.', - data: { - id: 'user-1', - username: 'testuser', - email: 'test@example.com', - profile: { - thumbnail: 'https://example.com/avatar.png', - }, - }, - error: null, - }, - }); + req.reply(BaseSuccess(userResponseData, '로그인에 성공하였습니다.')); } }).as('loginAPI'); - cy.intercept('POST', '**/api/login-sample', { - statusCode: 200, - body: { - success: true, - message: '샘플 로그인에 성공하였습니다.', - data: { - id: 'user-999', - username: 'sampleuser', - email: 'sample@example.com', - profile: { - thumbnail: 'https://example.com/sample-avatar.png', - }, - }, - error: null, - }, - }).as('sampleLoginAPI'); + cy.intercept( + 'POST', + '**/api/login-sample', + BaseSuccess(userResponseData, '샘플 로그인에 성공하였습니다.'), + ).as('sampleLoginAPI'); - cy.intercept('GET', '**/api/me', { - statusCode: 200, - body: { - success: true, - message: '사용자 정보 조회에 성공하였습니다.', - data: { - id: 'user-1', - username: 'testuser', - email: 'test@example.com', - profile: { - thumbnail: '/profile.jpg', - }, - }, - error: null, - }, - }).as('meAPI'); + cy.intercept( + 'GET', + '**/api/me', + BaseSuccess(userResponseData, '사용자 정보 조회에 성공하였습니다.'), + ).as('meAPI'); cy.intercept('GET', '**/api/posts*', (req) => { const url = new URL(req.url); const cursor = url.searchParams.get('cursor'); - if (!cursor) { - req.reply({ - statusCode: 200, - body: { - success: true, - message: '게시물 목록 조회에 성공하였습니다.', - data: { - nextCursor: '2025-01-09T00:00:00Z,10', - posts: [ - { - id: 1, - title: '테스트 게시물 1', - slug: 'test-post-1', - views: 150, - likes: 25, - yesterdayViews: 10, - yesterdayLikes: 5, - createdAt: '2025-01-08T10:00:00Z', - releasedAt: '2025-01-08T10:00:00Z', - }, - { - id: 2, - title: '테스트 게시물 2', - slug: 'test-post-2', - views: 200, - likes: 35, - yesterdayViews: 15, - yesterdayLikes: 8, - createdAt: '2025-01-07T15:30:00Z', - releasedAt: '2025-01-07T15:30:00Z', - }, - ], - }, - error: null, - }, - }); - } else { - req.reply({ - statusCode: 200, - body: { - success: true, - message: '게시물 목록 조회에 성공하였습니다.', - data: { - nextCursor: null, - posts: [ - { - id: 3, - title: '테스트 게시물 3', - slug: 'test-post-3', - views: 120, - likes: 18, - yesterdayViews: 8, - yesterdayLikes: 3, - createdAt: '2025-01-06T09:00:00Z', - releasedAt: '2025-01-06T09:00:00Z', - }, - { - id: 4, - title: '테스트 게시물 4', - slug: 'test-post-4', - views: 90, - likes: 12, - yesterdayViews: 5, - yesterdayLikes: 2, - createdAt: '2025-01-05T14:00:00Z', - releasedAt: '2025-01-05T14:00:00Z', - }, - ], - }, - error: null, - }, - }); - } + req.reply( + BaseSuccess(!cursor ? postsFirstData : postsSecondData, '게시물 목록 조회에 성공하였습니다.'), + ); }).as('postsAPI'); - cy.intercept('GET', '**/api/posts-stats', { - statusCode: 200, - body: { - success: true, - message: '게시물 통계 조회에 성공하였습니다.', - data: { - totalPostCount: 15, - stats: { - lastUpdatedDate: '2025-01-09T00:00:00Z', - totalLikes: 350, - totalViews: 2500, - yesterdayLikes: 45, - yesterdayViews: 180, - }, - }, - error: null, - }, - }).as('postsStatsAPI'); + cy.intercept( + 'GET', + '**/api/posts-stats', + BaseSuccess(postsStatsResponseData, '게시물 통계 조회에 성공하였습니다.'), + ).as('postsStatsAPI'); - cy.intercept('GET', '**/api/leaderboard/user*', { - statusCode: 200, - body: { - success: true, - message: '사용자 리더보드 조회에 성공하였습니다.', - data: { - users: [ - { - id: 'user-1', - email: 'user1@example.com', - username: 'topuser1', - totalViews: 15000, - totalLikes: 1200, - totalPosts: 45, - viewDiff: 500, - likeDiff: 50, - postDiff: 3, - }, - { - id: 'user-2', - email: 'user2@example.com', - username: 'topuser2', - totalViews: 12000, - totalLikes: 980, - totalPosts: 38, - viewDiff: 300, - likeDiff: 40, - postDiff: 2, - }, - { - id: 'user-3', - email: 'user3@example.com', - username: 'topuser3', - totalViews: 10000, - totalLikes: 800, - totalPosts: 30, - viewDiff: 250, - likeDiff: 35, - postDiff: 1, - }, - ], - }, - error: null, - }, - }).as('userLeaderboardAPI'); + cy.intercept( + 'GET', + '**/api/leaderboard/user*', + BaseSuccess(userLeaderboardResponseData, '사용자 리더보드 조회에 성공하였습니다.'), + ).as('userLeaderboardAPI'); - cy.intercept('GET', '**/api/leaderboard/post*', { - statusCode: 200, - body: { - success: true, - message: '게시물 리더보드 조회에 성공하였습니다.', - data: { - posts: [ - { - id: 'post-1', - title: '인기 게시물 1', - slug: 'popular-post-1', - username: 'author1', - totalViews: 5000, - totalLikes: 400, - viewDiff: 200, - likeDiff: 30, - releasedAt: '2025-01-07T10:00:00Z', - }, - { - id: 'post-2', - title: '인기 게시물 2', - slug: 'popular-post-2', - username: 'author2', - totalViews: 4500, - totalLikes: 350, - viewDiff: 150, - likeDiff: 25, - releasedAt: '2025-01-06T14:30:00Z', - }, - { - id: 'post-3', - title: '인기 게시물 3', - slug: 'popular-post-3', - username: 'author3', - totalViews: 4000, - totalLikes: 300, - viewDiff: 120, - likeDiff: 20, - releasedAt: '2025-01-05T09:15:00Z', - }, - ], - }, - error: null, - }, - }).as('postLeaderboardAPI'); + cy.intercept( + 'GET', + '**/api/leaderboard/post*', + BaseSuccess(postLeaderboardResponseData, '게시물 리더보드 조회에 성공하였습니다.'), + ).as('postLeaderboardAPI'); - cy.intercept('GET', '**/api/total-stats*', { - statusCode: 200, - body: { - success: true, - message: '전체 통계 조회에 성공하였습니다.', - data: [ - { date: '2025-01-03T00:00:00Z', value: 100 }, - { date: '2025-01-04T00:00:00Z', value: 150 }, - { date: '2025-01-05T00:00:00Z', value: 200 }, - { date: '2025-01-06T00:00:00Z', value: 180 }, - { date: '2025-01-07T00:00:00Z', value: 250 }, - { date: '2025-01-08T00:00:00Z', value: 300 }, - { date: '2025-01-09T00:00:00Z', value: 350 }, - ], - error: null, - }, - }).as('totalStatsAPI'); + cy.intercept( + 'GET', + '**/api/total-stats*', + BaseSuccess(totalStatsResponseData, '전체 통계 조회에 성공하였습니다.'), + ).as('totalStatsAPI'); - cy.intercept('GET', '**/api/notis', { - statusCode: 200, - body: { - success: true, - message: '공지사항 조회에 성공하였습니다.', - data: { - posts: [ - { - id: 'noti-1', - title: '시스템 점검 안내', - content: '시스템 점검이 예정되어 있습니다.', - createdAt: '2025-01-08T09:00:00Z', - isImportant: true, - }, - { - id: 'noti-2', - title: '새로운 기능 업데이트', - content: '새로운 기능이 추가되었습니다.', - createdAt: '2025-01-07T16:00:00Z', - isImportant: false, - }, - ], - }, - error: null, - }, - }).as('notisAPI'); + cy.intercept( + 'GET', + '**/api/notis', + BaseSuccess(notificationsResponseData, '공지사항 조회에 성공하였습니다.'), + ).as('notisAPI'); - cy.intercept('POST', '**/api/logout', { - statusCode: 200, - body: { - success: true, - message: '로그아웃에 성공하였습니다.', - data: {}, - error: null, - }, - }).as('logoutAPI'); + cy.intercept('POST', '**/api/logout', BaseSuccess({}, '성공적으로 로그아웃되었습니다.')).as( + 'logoutAPI', + ); - cy.intercept('GET', '**/api/post/**', { - statusCode: 200, - body: { - success: true, - message: '게시물 상세 정보 조회에 성공하였습니다.', - data: { - post: [ - { date: '2025-01-03T00:00:00Z', dailyViewCount: 20, dailyLikeCount: 2 }, - { date: '2025-01-04T00:00:00Z', dailyViewCount: 35, dailyLikeCount: 5 }, - { date: '2025-01-05T00:00:00Z', dailyViewCount: 45, dailyLikeCount: 8 }, - { date: '2025-01-06T00:00:00Z', dailyViewCount: 30, dailyLikeCount: 4 }, - { date: '2025-01-07T00:00:00Z', dailyViewCount: 60, dailyLikeCount: 12 }, - { date: '2025-01-08T00:00:00Z', dailyViewCount: 80, dailyLikeCount: 15 }, - { date: '2025-01-09T00:00:00Z', dailyViewCount: 100, dailyLikeCount: 20 }, - ], - }, - error: null, - }, - }).as('postDetailAPI'); + cy.intercept( + 'GET', + '**/api/post/**', + BaseSuccess(postsGraphData, '게시물 상세 정보 조회에 성공하였습니다.'), + ).as('postDetailAPI'); }); /* eslint-disable @typescript-eslint/no-namespace */ diff --git a/cypress/support/index.ts b/cypress/support/index.ts new file mode 100644 index 0000000..0ad4e08 --- /dev/null +++ b/cypress/support/index.ts @@ -0,0 +1,4 @@ +export * from './base'; +export * from './mock'; +export * from './commands'; +export * from './e2e'; diff --git a/cypress/support/mock.ts b/cypress/support/mock.ts new file mode 100644 index 0000000..8eb5024 --- /dev/null +++ b/cypress/support/mock.ts @@ -0,0 +1,249 @@ +// API 서버의 응답 형태에 맞춘 모킹 데이터 + +export const MOCK_ACCESS_TOKEN = 'mock_access_token_12345'; +export const MOCK_REFRESH_TOKEN = 'mock_refresh_token_67890'; + +// 유저 정보 관련 응답 데이터 +export const userResponseData = { + id: 1, + username: 'testuser', + email: 'test@example.com', + profile: { thumbnail: '/profile.jpg' }, +}; + +// 게시물 목록 응답 데이터 +export const postsFirstData = { + nextCursor: '2025-01-09T00:00:00Z,10', + posts: [ + { + id: 1, + title: '테스트 게시물 1', + slug: 'test-post-1', + views: 150, + likes: 25, + yesterdayViews: 10, + yesterdayLikes: 5, + createdAt: '2025-01-08T10:00:00Z', + releasedAt: '2025-01-08T10:00:00Z', + }, + { + id: 2, + title: '테스트 게시물 2', + slug: 'test-post-2', + views: 200, + likes: 35, + yesterdayViews: 15, + yesterdayLikes: 8, + createdAt: '2025-01-07T15:30:00Z', + releasedAt: '2025-01-07T15:30:00Z', + }, + ], +}; + +export const postsSecondData = { + nextCursor: null, + posts: [ + { + id: 3, + title: '테스트 게시물 3', + slug: 'test-post-3', + views: 120, + likes: 18, + yesterdayViews: 8, + yesterdayLikes: 3, + createdAt: '2025-01-06T09:00:00Z', + releasedAt: '2025-01-06T09:00:00Z', + }, + { + id: 4, + title: '테스트 게시물 4', + slug: 'test-post-4', + views: 90, + likes: 12, + yesterdayViews: 5, + yesterdayLikes: 2, + createdAt: '2025-01-05T14:00:00Z', + releasedAt: '2025-01-05T14:00:00Z', + }, + ], +}; + +// 게시물 목록 응답 데이터 +export const postsGraphData = { + post: [ + { date: '2025-01-03T00:00:00Z', dailyViewCount: 20, dailyLikeCount: 2 }, + { date: '2025-01-04T00:00:00Z', dailyViewCount: 35, dailyLikeCount: 5 }, + { date: '2025-01-05T00:00:00Z', dailyViewCount: 45, dailyLikeCount: 8 }, + { date: '2025-01-06T00:00:00Z', dailyViewCount: 30, dailyLikeCount: 4 }, + { date: '2025-01-07T00:00:00Z', dailyViewCount: 60, dailyLikeCount: 12 }, + { date: '2025-01-08T00:00:00Z', dailyViewCount: 80, dailyLikeCount: 15 }, + { date: '2025-01-09T00:00:00Z', dailyViewCount: 100, dailyLikeCount: 20 }, + ], +}; + +// 게시물 통계 응답 데이터 +export const postsStatsResponseData = { + totalPostCount: 15, + stats: { + lastUpdatedDate: '2025-01-09T00:00:00Z', + totalLikes: 350, + totalViews: 2500, + yesterdayLikes: 45, + yesterdayViews: 180, + }, +}; + +// 게시물 상세 응답 데이터 생성 함수 (일일 통계 배열) +export const createPostDetailResponseData = (postId: string) => ({ + post: [ + { + date: '2025-01-07T00:00:00Z', + dailyViewCount: 100 + parseInt(postId) * 10, + dailyLikeCount: 15 + parseInt(postId) * 2, + }, + { + date: '2025-01-08T00:00:00Z', + dailyViewCount: 150 + parseInt(postId) * 10, + dailyLikeCount: 25 + parseInt(postId) * 2, + }, + ], +}); + +// UUID 기반 게시물 상세 응답 데이터 생성 함수 (일일 통계 배열) +export const createPostByUUIDResponseData = (postId: string) => ({ + post: [ + { + date: '2025-01-07T00:00:00Z', + dailyViewCount: 150 + postId.length * 5, + dailyLikeCount: 25 + postId.length * 2, + }, + { + date: '2025-01-08T00:00:00Z', + dailyViewCount: 175 + postId.length * 5, + dailyLikeCount: 35 + postId.length * 2, + }, + ], +}); + +// 사용자 리더보드 응답 데이터 +export const userLeaderboardResponseData = { + users: [ + { + id: 'user-1', + email: 'user1@example.com', + username: 'topuser1', + totalViews: 15000, + totalLikes: 1200, + totalPosts: 45, + viewDiff: 500, + likeDiff: 50, + postDiff: 3, + }, + { + id: 'user-2', + email: 'user2@example.com', + username: 'topuser2', + totalViews: 12000, + totalLikes: 980, + totalPosts: 38, + viewDiff: 300, + likeDiff: 40, + postDiff: 2, + }, + { + id: 'user-3', + email: 'user3@example.com', + username: 'topuser3', + totalViews: 10000, + totalLikes: 800, + totalPosts: 30, + viewDiff: 250, + likeDiff: 35, + postDiff: 1, + }, + ], +}; + +// 게시물 리더보드 응답 데이터 +export const postLeaderboardResponseData = { + posts: [ + { + id: 'post-1', + title: '인기 게시물 1', + slug: 'popular-post-1', + username: 'author1', + totalViews: 5000, + totalLikes: 400, + viewDiff: 200, + likeDiff: 30, + releasedAt: '2025-01-07T10:00:00Z', + }, + { + id: 'post-2', + title: '인기 게시물 2', + slug: 'popular-post-2', + username: 'author2', + totalViews: 4500, + totalLikes: 350, + viewDiff: 150, + likeDiff: 25, + releasedAt: '2025-01-06T14:30:00Z', + }, + { + id: 'post-3', + title: '인기 게시물 3', + slug: 'popular-post-3', + username: 'author3', + totalViews: 4000, + totalLikes: 300, + viewDiff: 120, + likeDiff: 20, + releasedAt: '2025-01-05T09:15:00Z', + }, + ], +}; + +// 전체 통계 응답 데이터 +export const totalStatsResponseData = [ + { date: '2025-01-03T00:00:00Z', value: 100 }, + { date: '2025-01-04T00:00:00Z', value: 150 }, + { date: '2025-01-05T00:00:00Z', value: 200 }, + { date: '2025-01-06T00:00:00Z', value: 180 }, + { date: '2025-01-07T00:00:00Z', value: 250 }, + { date: '2025-01-08T00:00:00Z', value: 300 }, + { date: '2025-01-09T00:00:00Z', value: 350 }, +]; + +// 공지사항 응답 데이터 +export const notificationsResponseData = { + posts: [ + { + id: 'noti-1', + title: '시스템 점검 안내', + content: '시스템 점검이 예정되어 있습니다.', + createdAt: '2025-01-08T09:00:00Z', + isImportant: true, + }, + { + id: 'noti-2', + title: '새로운 기능 업데이트', + content: '새로운 기능이 추가되었습니다.', + createdAt: '2025-01-07T16:00:00Z', + isImportant: false, + }, + ], +}; + +// 전체 통계 타입별 성공 메시지 +export const getTotalStatsMessage = (type: string) => { + switch (type) { + case 'view': + return '전체 조회수 변동 조회에 성공하였습니다.'; + case 'like': + return '전체 좋아요 변동 조회에 성공하였습니다.'; + case 'post': + return '전체 게시글 수 변동 조회에 성공하였습니다.'; + default: + return '전체 통계 조회에 성공하였습니다.'; + } +}; From dc5406befaade31cf283374be5109c8708452d0c Mon Sep 17 00:00:00 2001 From: six-standard Date: Sun, 13 Jul 2025 23:20:31 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=98=A4=EC=9E=91=EB=8F=99=20=EA=B4=80=EB=A0=A8=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cypress/e2e/main.cy.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/main.cy.ts b/cypress/e2e/main.cy.ts index bda8d2d..0ea48a6 100644 --- a/cypress/e2e/main.cy.ts +++ b/cypress/e2e/main.cy.ts @@ -16,7 +16,7 @@ describe('메인 페이지', () => { cy.contains('전체 좋아요 수').should('be.visible'); cy.contains('총 게시글 수').should('be.visible'); - cy.contains('2500').should('be.visible'); + cy.contains('2,500').should('be.visible'); cy.contains('350').should('be.visible'); cy.contains('15').should('be.visible'); }); @@ -25,20 +25,26 @@ describe('메인 페이지', () => { cy.contains('테스트 게시물 1').should('be.visible'); cy.contains('테스트 게시물 2').should('be.visible'); - cy.contains('150').should('be.visible'); - cy.contains('25').should('be.visible'); + cy.get('section').should('contain.text', '150'); + cy.get('section').should('contain.text', '25'); + cy.get('section').should('contain.text', '200'); + cy.get('section').should('contain.text', '35'); }); it('정렬 및 필터 기능이 동작해야 한다', () => { - cy.get('select').should('be.visible'); - cy.get('input[type="checkbox"]').should('be.visible'); + cy.get('button').should('exist'); + + cy.get('input[type="checkbox"]').should('exist'); + cy.contains('오름차순').should('be.visible'); + cy.contains('새로고침').should('be.visible'); cy.contains('새로고침').should('be.disabled'); }); it('마지막 업데이트 시간이 표시되어야 한다', () => { - cy.contains('마지막 업데이트').should('be.visible'); - cy.contains(/\d{4}-\d{2}-\d{2}/).should('be.visible'); + cy.contains('마지막 업데이트').should('exist'); + + cy.get('body').should('contain.text', '2025'); }); it('로그아웃 기능이 동작해야 한다', () => { From d961a57e917aa05d1912f05d1662a9e51c256e2a Mon Sep 17 00:00:00 2001 From: six-standard Date: Wed, 16 Jul 2025 13:08:44 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=EB=90=9C?= =?UTF-8?q?=20Math.floor=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/datetime.util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/datetime.util.ts b/src/utils/datetime.util.ts index b03c801..4003e76 100644 --- a/src/utils/datetime.util.ts +++ b/src/utils/datetime.util.ts @@ -55,7 +55,7 @@ export const convertDateToKST = (date?: string): KSTDateFormat | undefined => { */ export const formatTimeToMMSS = (time: number) => { - const minute = Math.floor(Math.floor(time) / 60) + const minute = Math.floor(time / 60) .toString() .padStart(2, '0'); const second = (Math.floor(time) % 60).toString().padStart(2, '0'); From 804589f84e21974fe492142cb00aa8373cb65c81 Mon Sep 17 00:00:00 2001 From: six-standard Date: Wed, 16 Jul 2025 13:09:00 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor:=20jest=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.ts | 2 +- jest.setup.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 jest.setup.ts diff --git a/jest.config.ts b/jest.config.ts index 80932b1..5e3762b 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -7,7 +7,7 @@ const createJestConfig = nextJest({ // Add any custom config to be passed to Jest const customJestConfig = { - setupFilesAfterEnv: ['/jest.setup.ts', '/setupTests.ts'], + setupFilesAfterEnv: [], testEnvironment: 'jest-fixed-jsdom', testEnvironmentOptions: { customExportConditions: [''], diff --git a/jest.setup.ts b/jest.setup.ts deleted file mode 100644 index 8b19a3d..0000000 --- a/jest.setup.ts +++ /dev/null @@ -1,4 +0,0 @@ -import '@testing-library/jest-dom'; -import fetchMock from 'jest-fetch-mock'; - -fetchMock.enableMocks();