// import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import { vi } from "vitest"; import { SheetViewer } from "../SheetViewer"; import { useAppStore } from "../../../stores/useAppStore"; import type { SheetData } from "../../../types/sheet"; // Mock dependencies vi.mock("../../../stores/useAppStore"); // Luckysheet 모킹 const mockLuckysheet = { create: vi.fn(), destroy: vi.fn(), resize: vi.fn(), getSheet: vi.fn(), getAllSheets: vi.fn(), setActiveSheet: vi.fn(), }; // Window.luckysheet 모킹 Object.defineProperty(window, "luckysheet", { value: mockLuckysheet, writable: true, }); // useAppStore 모킹 타입 const mockUseAppStore = vi.mocked(useAppStore); // 기본 스토어 상태 const defaultStoreState = { sheets: [], activeSheetId: null, currentFile: null, setSelectedRange: vi.fn(), isLoading: false, error: null, setLoading: vi.fn(), setError: vi.fn(), uploadFile: vi.fn(), clearFileUploadErrors: vi.fn(), resetApp: vi.fn(), }; // 테스트용 시트 데이터 const mockSheetData: SheetData[] = [ { id: "sheet_0", name: "Sheet1", data: [ ["A1", "B1", "C1"], ["A2", "B2", "C2"], ], config: { container: "luckysheet_0", title: "Sheet1", lang: "ko", data: [ { name: "Sheet1", index: "0", celldata: [ { r: 0, c: 0, v: { v: "A1", m: "A1", ct: { fa: "General", t: "g" } }, }, { r: 0, c: 1, v: { v: "B1", m: "B1", ct: { fa: "General", t: "g" } }, }, ], status: 1, order: 0, row: 2, column: 3, }, ], options: { showtoolbar: true, showinfobar: false, showsheetbar: true, showstatisticBar: false, allowCopy: true, allowEdit: true, enableAddRow: true, enableAddCol: true, }, }, }, ]; describe("SheetViewer", () => { beforeEach(() => { vi.clearAllMocks(); mockUseAppStore.mockReturnValue(defaultStoreState); }); afterEach(() => { // DOM 정리 document.head.innerHTML = ""; }); describe("초기 렌더링", () => { it("시트 데이터가 없을 때 적절한 메시지를 표시한다", () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: [], }); render(); expect(screen.getByText("표시할 시트가 없습니다")).toBeInTheDocument(); expect( screen.getByText("Excel 파일을 업로드해주세요."), ).toBeInTheDocument(); }); it("시트 데이터가 있을 때 로딩 상태를 표시한다", () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); render(); expect(screen.getByText("시트 로딩 중...")).toBeInTheDocument(); expect(screen.getByText("잠시만 기다려주세요.")).toBeInTheDocument(); }); it("Luckysheet 컨테이너가 올바르게 렌더링된다", () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); render(); const container = document.getElementById("luckysheet-container"); expect(container).toBeInTheDocument(); expect(container).toHaveClass("w-full", "h-full"); }); }); describe("Luckysheet 초기화", () => { it("시트 데이터가 변경되면 Luckysheet를 초기화한다", async () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); render(); await waitFor(() => { expect(mockLuckysheet.create).toHaveBeenCalled(); }); // create 호출 시 전달된 설정 확인 const createCall = mockLuckysheet.create.mock.calls[0]; expect(createCall).toBeDefined(); const config = createCall[0]; expect(config.container).toBe("luckysheet-container"); expect(config.title).toBe("test.xlsx"); expect(config.lang).toBe("ko"); expect(config.data).toHaveLength(1); }); it("기존 Luckysheet 인스턴스가 있으면 제거한다", async () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); const { rerender } = render(); await waitFor(() => { expect(mockLuckysheet.create).toHaveBeenCalledTimes(1); }); // 시트 데이터 변경 const newSheetData: SheetData[] = [ { ...mockSheetData[0], name: "NewSheet", }, ]; mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: newSheetData, currentFile: { name: "new.xlsx", size: 1000, uploadedAt: new Date() }, }); rerender(); await waitFor(() => { expect(mockLuckysheet.destroy).toHaveBeenCalled(); expect(mockLuckysheet.create).toHaveBeenCalledTimes(2); }); }); }); describe("에러 처리", () => { it("Luckysheet 초기화 실패 시 에러 메시지를 표시한다", async () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); // Luckysheet.create에서 에러 발생 시뮬레이션 mockLuckysheet.create.mockImplementation(() => { throw new Error("Luckysheet 초기화 실패"); }); render(); await waitFor(() => { expect(screen.getByText("시트 로드 오류")).toBeInTheDocument(); expect( screen.getByText(/시트 초기화에 실패했습니다/), ).toBeInTheDocument(); }); // 다시 시도 버튼 확인 const retryButton = screen.getByRole("button", { name: "다시 시도" }); expect(retryButton).toBeInTheDocument(); }); it("다시 시도 버튼을 클릭하면 초기화를 재시도한다", async () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); // 첫 번째 시도에서 실패 mockLuckysheet.create.mockImplementationOnce(() => { throw new Error("첫 번째 실패"); }); render(); await waitFor(() => { expect(screen.getByText("시트 로드 오류")).toBeInTheDocument(); }); // 두 번째 시도에서 성공하도록 설정 mockLuckysheet.create.mockImplementationOnce(() => { // 성공 }); const retryButton = screen.getByRole("button", { name: "다시 시도" }); retryButton.click(); await waitFor(() => { expect(mockLuckysheet.create).toHaveBeenCalledTimes(2); }); }); }); describe("이벤트 핸들링", () => { it("셀 클릭 시 선택된 범위를 스토어에 저장한다", async () => { const mockSetSelectedRange = vi.fn(); mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, setSelectedRange: mockSetSelectedRange, }); render(); await waitFor(() => { expect(mockLuckysheet.create).toHaveBeenCalled(); }); // create 호출 시 전달된 hook 확인 const createCall = mockLuckysheet.create.mock.calls[0]; const config = createCall[0]; // cellClick 핸들러 시뮬레이션 const cellClickHandler = config.hook.cellClick; expect(cellClickHandler).toBeDefined(); const mockCell = {}; const mockPosition = { r: 1, c: 2 }; const mockSheetFile = { index: "0" }; cellClickHandler(mockCell, mockPosition, mockSheetFile); expect(mockSetSelectedRange).toHaveBeenCalledWith({ range: { startRow: 1, startCol: 2, endRow: 1, endCol: 2, }, sheetId: "0", }); }); it("시트 활성화 시 활성 시트 ID를 업데이트한다", async () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); // setActiveSheetId를 spy로 설정 const setActiveSheetIdSpy = vi.fn(); useAppStore.getState = vi.fn().mockReturnValue({ setActiveSheetId: setActiveSheetIdSpy, }); render(); await waitFor(() => { expect(mockLuckysheet.create).toHaveBeenCalled(); }); // create 호출 시 전달된 hook 확인 const createCall = mockLuckysheet.create.mock.calls[0]; const config = createCall[0]; // sheetActivate 핸들러 시뮬레이션 const sheetActivateHandler = config.hook.sheetActivate; expect(sheetActivateHandler).toBeDefined(); sheetActivateHandler(0, false, false); expect(setActiveSheetIdSpy).toHaveBeenCalledWith("sheet_0"); }); }); describe("컴포넌트 생명주기", () => { it("컴포넌트 언마운트 시 Luckysheet를 정리한다", async () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); const { unmount } = render(); await waitFor(() => { expect(mockLuckysheet.create).toHaveBeenCalled(); }); unmount(); expect(mockLuckysheet.destroy).toHaveBeenCalled(); }); it("윈도우 리사이즈 시 Luckysheet 리사이즈를 호출한다", async () => { mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); render(); await waitFor(() => { expect(mockLuckysheet.create).toHaveBeenCalled(); }); // 윈도우 리사이즈 이벤트 시뮬레이션 window.dispatchEvent(new Event("resize")); expect(mockLuckysheet.resize).toHaveBeenCalled(); }); }); describe("개발 모드 정보", () => { it("개발 모드에서 시트 정보를 표시한다", () => { const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = "development"; mockUseAppStore.mockReturnValue({ ...defaultStoreState, sheets: mockSheetData, activeSheetId: "sheet_0", currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() }, }); render(); expect(screen.getByText("시트 개수: 1")).toBeInTheDocument(); expect(screen.getByText("활성 시트: sheet_0")).toBeInTheDocument(); process.env.NODE_ENV = originalEnv; }); }); });