import { describe, it, expect, afterEach, vi, beforeEach } from "vitest"; import { render, cleanup } from "@testing-library/react"; import EditorPage from "@/app/editor/page"; // EditorPage 전역 단축키 TDD // - Undo/Redo, 삭제/복제, 선택 이동(ArrowUp/Down) 단축키가 editorStore 액션을 호출하는지 검증한다. // - 입력 포커스가 input/textarea 인 경우에는 단축키가 동작하지 않아야 한다. const undo = vi.fn(); const redo = vi.fn(); const removeBlock = vi.fn(); const duplicateBlock = vi.fn(); const selectBlock = vi.fn(); let mockState: any; beforeEach(() => { mockState = { // 블록 2개를 가진 기본 상태 blocks: [ { id: "blk_1", type: "text", props: { text: "첫 번째" } }, { id: "blk_2", type: "text", props: { text: "두 번째" } }, ], selectedBlockId: "blk_1", selectedListItemId: null, undo, redo, removeBlock, duplicateBlock, selectBlock, // EditorPage 가 참조하지만 이 테스트에서 중요하지 않은 액션들: no-op addTextBlock: vi.fn(), addButtonBlock: vi.fn(), addImageBlock: vi.fn(), addDividerBlock: vi.fn(), addListBlock: vi.fn(), addSectionBlock: vi.fn(), addFormBlock: vi.fn(), addFormInputBlock: vi.fn(), addFormSelectBlock: vi.fn(), addFormCheckboxBlock: vi.fn(), addFormRadioBlock: vi.fn(), addHeroTemplateSection: vi.fn(), addFeaturesTemplateSection: vi.fn(), addCtaTemplateSection: vi.fn(), addFaqTemplateSection: vi.fn(), addPricingTemplateSection: vi.fn(), addTestimonialsTemplateSection: vi.fn(), addBlogTemplateSection: vi.fn(), addTeamTemplateSection: vi.fn(), addFooterTemplateSection: vi.fn(), updateBlock: vi.fn(), selectListItem: vi.fn(), indentSelectedListItem: vi.fn(), outdentSelectedListItem: vi.fn(), moveSelectedListItemUp: vi.fn(), moveSelectedListItemDown: vi.fn(), replaceBlocks: vi.fn(), reorderBlocks: vi.fn(), moveBlock: vi.fn(), projectConfig: { title: "테스트 페이지", slug: "test-page", canvasPreset: "full", }, }; undo.mockReset(); redo.mockReset(); removeBlock.mockReset(); duplicateBlock.mockReset(); selectBlock.mockReset(); }); vi.mock("@/features/editor/state/editorStore", () => { const useEditorStore = (selector: (state: any) => any) => selector(mockState); (useEditorStore as any).getState = () => mockState; return { __esModule: true, useEditorStore, }; }); vi.mock("next/link", () => { return { __esModule: true, default: ({ href, children }: any) => {children}, }; }); afterEach(() => { cleanup(); }); describe("EditorPage - 전역 단축키", () => { it("Cmd/Ctrl+Z 는 undo 액션을 호출해야 한다", () => { // Mac 플랫폼으로 강제 설정 const originalPlatform = Object.getOwnPropertyDescriptor(navigator, "platform"); Object.defineProperty(navigator, "platform", { value: "MacIntel", configurable: true }); render(); const event = new KeyboardEvent("keydown", { key: "z", metaKey: true }); window.dispatchEvent(event); expect(undo).toHaveBeenCalledTimes(1); if (originalPlatform) { Object.defineProperty(navigator, "platform", originalPlatform); } }); it("Cmd/Ctrl+Shift+Z 는 redo 액션을 호출해야 한다", () => { const originalPlatform = Object.getOwnPropertyDescriptor(navigator, "platform"); Object.defineProperty(navigator, "platform", { value: "MacIntel", configurable: true }); render(); const event = new KeyboardEvent("keydown", { key: "z", metaKey: true, shiftKey: true }); window.dispatchEvent(event); expect(redo).toHaveBeenCalledTimes(1); if (originalPlatform) { Object.defineProperty(navigator, "platform", originalPlatform); } }); it("Delete/Backspace 는 선택된 블록을 removeBlock 으로 삭제해야 한다", () => { mockState.selectedBlockId = "blk_1"; render(); const event = new KeyboardEvent("keydown", { key: "Backspace" }); window.dispatchEvent(event); expect(removeBlock).toHaveBeenCalledTimes(1); expect(removeBlock).toHaveBeenCalledWith("blk_1"); }); it("Cmd/Ctrl+D 는 선택된 블록을 duplicateBlock 으로 복제해야 한다", () => { const originalPlatform = Object.getOwnPropertyDescriptor(navigator, "platform"); Object.defineProperty(navigator, "platform", { value: "MacIntel", configurable: true }); mockState.selectedBlockId = "blk_2"; render(); const event = new KeyboardEvent("keydown", { key: "d", metaKey: true }); window.dispatchEvent(event); expect(duplicateBlock).toHaveBeenCalledTimes(1); expect(duplicateBlock).toHaveBeenCalledWith("blk_2"); if (originalPlatform) { Object.defineProperty(navigator, "platform", originalPlatform); } }); it("ArrowDown 은 선택이 없을 때 첫 번째 블록을 선택해야 한다", () => { mockState.selectedBlockId = null; render(); const event = new KeyboardEvent("keydown", { key: "ArrowDown" }); window.dispatchEvent(event); expect(selectBlock).toHaveBeenCalledTimes(1); expect(selectBlock).toHaveBeenCalledWith("blk_1"); }); it("ArrowUp 은 선택이 없을 때 마지막 블록을 선택해야 한다", () => { mockState.selectedBlockId = null; render(); const event = new KeyboardEvent("keydown", { key: "ArrowUp" }); window.dispatchEvent(event); expect(selectBlock).toHaveBeenCalledTimes(1); expect(selectBlock).toHaveBeenCalledWith("blk_2"); }); it("입력 포커스(input) 상태에서는 Backspace 로 removeBlock 이 호출되면 안 된다", () => { mockState.selectedBlockId = "blk_1"; render(); const input = document.createElement("input"); document.body.appendChild(input); const event = new KeyboardEvent("keydown", { key: "Backspace", bubbles: true }); Object.defineProperty(event, "target", { value: input, configurable: true }); window.dispatchEvent(event); expect(removeBlock).not.toHaveBeenCalled(); document.body.removeChild(input); }); });