/** * Vitest 테스트 환경 설정 */ import '@testing-library/jest-dom'; import { vi } from 'vitest'; // 전역 mocks global.fetch = vi.fn(); /** * IntersectionObserver 폴리필 — jsdom은 IntersectionObserver를 기본 제공하지 않으므로 * 컴포넌트가 무한스크롤/가시성 추적을 위해 사용하는 경우를 대비해 no-op 폴리필을 전역 제공. * 개별 테스트에서 세부 동작 제어가 필요하면 `vi.stubGlobal('IntersectionObserver', ...)` 으로 덮어쓴다. */ class NoopIntersectionObserver { root: Element | null = null; rootMargin = ''; thresholds: number[] = []; observe(): void {} unobserve(): void {} disconnect(): void {} takeRecords(): IntersectionObserverEntry[] { return []; } } (global as any).IntersectionObserver = NoopIntersectionObserver; (window as any).IntersectionObserver = NoopIntersectionObserver; /** * window.matchMedia 모킹 * 테스트 환경에서 CSS 미디어 쿼리를 지원하기 위함 */ Object.defineProperty(window, 'matchMedia', { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }); /** * 테스트용 번역 맵 * 컴포넌트에서 사용하는 번역 키에 대한 한글 번역 */ const testTranslations: Record = { // 공통 'common.close': '닫기', 'common.close_modal': '모달 닫기', 'common.close_dialog': '대화상자 닫기', 'common.close_notification': '알림 닫기', 'common.confirm': '확인', 'common.cancel': '취소', 'common.save': '저장', 'common.delete': '삭제', 'common.edit': '편집', 'common.edit_layout': '레이아웃 편집', 'common.search': '검색', 'common.search_placeholder': '검색...', 'common.loading': '로딩 중...', 'common.first_page': '첫 페이지', 'common.last_page': '마지막 페이지', 'common.previous_page': '이전 페이지', 'common.prev_page': '이전 페이지', 'common.next_page': '다음 페이지', 'common.page': '페이지', 'common.page_n': '페이지 {{n}}', 'common.pagination': '페이지네이션', 'common.activate': '활성화', 'common.deactivate': '비활성화', 'common.enabled': '활성화됨', 'common.disabled': '비활성화됨', 'common.update_available': '업데이트 가능', 'common.preview': '미리보기', 'common.dependencies': '의존성', 'common.no_data': '데이터가 없습니다', 'common.notifications': '알림', 'common.no_notifications': '알림이 없습니다', 'common.profile_settings': '프로필 설정', 'common.logout': '로그아웃', 'common.reset': '초기화', 'common.filter': '필터', // 상태 'common.status_active': '활성화', 'common.status_inactive': '비활성화', 'common.status_pending': '대기중', 'common.status_error': '오류', // 토스트 'toast.close': '알림 닫기', // 템플릿 'template.activate': '활성화', 'template.deactivate': '비활성화', 'template.update_available': '업데이트 가능', 'template.edit_layout': '레이아웃 편집', 'template.preview': '미리보기', 'common.template_preview': '{{vendor}}/{{name}} 미리보기', // 사이드바 'sidebar.collapse': '사이드바 접기', 'sidebar.expand': '사이드바 펼치기', 'common.collapse_sidebar': '사이드바 접기', 'common.expand_sidebar': '사이드바 펼치기', }; /** * G7Core 전역 모킹 * 컴포넌트에서 window.G7Core를 사용하므로 테스트 환경에서 모킹 필요 */ (window as any).G7Core = { createChangeEvent: vi.fn((data: { checked?: boolean; name?: string; value?: any }) => ({ target: { checked: data.checked ?? false, name: data.name ?? '', value: data.value ?? (data.checked ? 'on' : 'off'), type: 'checkbox', }, currentTarget: { checked: data.checked ?? false, name: data.name ?? '', value: data.value ?? (data.checked ? 'on' : 'off'), type: 'checkbox', }, preventDefault: vi.fn(), stopPropagation: vi.fn(), })), state: { get: vi.fn(), set: vi.fn(), update: vi.fn(), subscribe: vi.fn(() => vi.fn()), }, toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn(), }, modal: { open: vi.fn(), close: vi.fn(), }, events: { emit: vi.fn(), on: vi.fn(() => vi.fn()), off: vi.fn(), }, t: (key: string, params?: Record) => { // 번역 맵에서 찾거나, 키 자체를 반환 let translation = testTranslations[key] ?? key; // params가 있으면 치환 if (params) { Object.entries(params).forEach(([paramKey, paramValue]) => { translation = translation.replace(`{{${paramKey}}}`, String(paramValue)); }); } return translation; }, };