import { describe, it, expect, afterEach, vi } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import { BlocksSidebar } from "@/app/editor/panels/BlocksSidebar"; // BlocksSidebar 템플릿 버튼 TDD // - 각 템플릿 추가 버튼이 대응하는 editorStore 액션(add*TemplateSection)을 호출하는지 검증한다. const templateActions = { 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(), }; // BlocksSidebar 가 사용하는 나머지 액션들도 더미 함수로 채워둔다. const otherActions = { addTextBlock: vi.fn(), addButtonBlock: vi.fn(), addImageBlock: vi.fn(), addVideoBlock: 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(), }; const mockState = { ...templateActions, ...otherActions, }; vi.mock("@/features/editor/state/editorStore", () => { return { __esModule: true, // useEditorStore(selector) 형태로 사용되므로, selector 에 mockState 를 넘겨준다. useEditorStore: (selector: (state: typeof mockState) => unknown) => selector(mockState), }; }); describe("BlocksSidebar - 템플릿 버튼", () => { afterEach(() => { cleanup(); Object.values(templateActions).forEach((fn) => fn.mockReset()); }); it("Hero 템플릿 버튼 클릭 시 addHeroTemplateSection 이 호출되어야 한다", () => { render(); const button = screen.getByRole("button", { name: "Hero 템플릿" }); fireEvent.click(button); expect(templateActions.addHeroTemplateSection).toHaveBeenCalledTimes(1); }); it("Features 템플릿 버튼 클릭 시 addFeaturesTemplateSection 이 호출되어야 한다", () => { render(); const button = screen.getByRole("button", { name: "기능 템플릿" }); fireEvent.click(button); expect(templateActions.addFeaturesTemplateSection).toHaveBeenCalledTimes(1); }); it("CTA 템플릿 버튼 클릭 시 addCtaTemplateSection 이 호출되어야 한다", () => { render(); const button = screen.getByRole("button", { name: "CTA 템플릿" }); fireEvent.click(button); expect(templateActions.addCtaTemplateSection).toHaveBeenCalledTimes(1); }); it("FAQ/Pricing/Testimonials/Blog/Team/Footer 템플릿 버튼도 각각의 액션을 호출해야 한다", () => { render(); fireEvent.click(screen.getByRole("button", { name: "FAQ 템플릿" })); fireEvent.click(screen.getByRole("button", { name: "상품 템플릿" })); fireEvent.click(screen.getByRole("button", { name: "후기 템플릿" })); fireEvent.click(screen.getByRole("button", { name: "블로그 템플릿" })); fireEvent.click(screen.getByRole("button", { name: "Team 템플릿" })); fireEvent.click(screen.getByRole("button", { name: "Footer 템플릿" })); expect(templateActions.addFaqTemplateSection).toHaveBeenCalledTimes(1); expect(templateActions.addPricingTemplateSection).toHaveBeenCalledTimes(1); expect(templateActions.addTestimonialsTemplateSection).toHaveBeenCalledTimes(1); expect(templateActions.addBlogTemplateSection).toHaveBeenCalledTimes(1); expect(templateActions.addTeamTemplateSection).toHaveBeenCalledTimes(1); expect(templateActions.addFooterTemplateSection).toHaveBeenCalledTimes(1); }); it("템플릿 섹션에는 각 템플릿의 용도를 설명하는 텍스트가 함께 표시되어야 한다", () => { render(); expect(screen.getByText("페이지 상단 Hero 섹션 (큰 제목 + 서브텍스트 + 버튼)")); expect(screen.getByText("3컬럼 기능 소개 섹션")); expect(screen.getByText("콜투액션(CTA) 섹션")); expect(screen.getByText("자주 묻는 질문(FAQ) 섹션")); expect(screen.getByText("요금제/플랜 소개 섹션")); expect(screen.getByText("고객 후기(Testimonials) 섹션")); expect(screen.getByText("블로그 포스트 목록 섹션")); expect(screen.getByText("팀 소개 섹션")); expect(screen.getByText("페이지 푸터 섹션")); }); it("비디오 버튼 클릭 시 addVideoBlock 이 호출되어야 한다", () => { render(); const button = screen.getByRole("button", { name: "비디오" }); fireEvent.click(button); expect(otherActions.addVideoBlock).toHaveBeenCalledTimes(1); }); it("템플릿들은 카테고리별로 그룹 헤더를 가져야 한다", () => { render(); expect(screen.getByText("히어로 · CTA")).toBeDefined(); expect(screen.getByText("콘텐츠 섹션")).toBeDefined(); expect(screen.getByText("신뢰/소개")).toBeDefined(); expect(screen.getByText("푸터/기타")).toBeDefined(); }); it("각 템플릿 카드에는 미니 썸네일(레이아웃 힌트)가 렌더링되어야 한다", () => { render(); expect(screen.getByTestId("template-preview-hero")).toBeDefined(); expect(screen.getByTestId("template-preview-features")).toBeDefined(); expect(screen.getByTestId("template-preview-cta")).toBeDefined(); expect(screen.getByTestId("template-preview-faq")).toBeDefined(); expect(screen.getByTestId("template-preview-pricing")).toBeDefined(); expect(screen.getByTestId("template-preview-testimonials")).toBeDefined(); expect(screen.getByTestId("template-preview-blog")).toBeDefined(); expect(screen.getByTestId("template-preview-team")).toBeDefined(); expect(screen.getByTestId("template-preview-footer")).toBeDefined(); }); it("미니 썸네일은 각 템플릿 레이아웃을 암시하는 구조를 가져야 한다", () => { render(); const heroThumb = screen.getByTestId("template-preview-hero"); const ctaThumb = screen.getByTestId("template-preview-cta"); const featuresThumb = screen.getByTestId("template-preview-features"); const pricingThumb = screen.getByTestId("template-preview-pricing"); const blogThumb = screen.getByTestId("template-preview-blog"); const teamThumb = screen.getByTestId("template-preview-team"); // Hero/CTA: 하나의 메인 영역을 가진 1열 구조 expect(heroThumb.children.length).toBe(1); expect(ctaThumb.children.length).toBe(1); // Features/Blog: 3컬럼 레이아웃을 암시하는 3개의 세그먼트 expect(featuresThumb.children.length).toBe(3); expect(blogThumb.children.length).toBe(3); // Pricing/Team: 높이가 다른 3개의 바를 가진 3컬럼 카드 레이아웃 expect(pricingThumb.children.length).toBe(3); expect(teamThumb.children.length).toBe(3); }); it("블록 섹션 타이틀을 클릭하면 블록 버튼들을 접고 펼 수 있어야 한다", () => { render(); // 초기에는 텍스트 버튼이 보여야 한다. expect(screen.getByRole("button", { name: "텍스트" })).toBeDefined(); const blockToggle = screen.getByRole("button", { name: "블록" }); fireEvent.click(blockToggle); // 접힌 상태에서는 텍스트 버튼이 보이지 않아야 한다. expect(screen.queryByRole("button", { name: "텍스트" })).toBeNull(); // 다시 클릭하면 펼쳐져야 한다. fireEvent.click(blockToggle); expect(screen.getByRole("button", { name: "텍스트" })).toBeDefined(); }); it("폼 요소 섹션 타이틀을 클릭하면 폼 관련 버튼들을 접고 펼 수 있어야 한다", () => { render(); // 초기에는 폼 입력 버튼이 보여야 한다. expect(screen.getByRole("button", { name: "입력(Input)" })).toBeDefined(); const formToggle = screen.getByRole("button", { name: "폼 요소" }); fireEvent.click(formToggle); // 접힌 상태에서는 폼 입력 버튼이 보이지 않아야 한다. expect(screen.queryByRole("button", { name: "입력(Input)" })).toBeNull(); // 다시 클릭하면 펼쳐져야 한다. fireEvent.click(formToggle); expect(screen.getByRole("button", { name: "입력(Input)" })).toBeDefined(); }); it("템플릿 섹션 타이틀을 클릭하면 템플릿 카드들을 접고 펼 수 있어야 한다", () => { render(); // 초기에는 Hero 템플릿 버튼이 보여야 한다. expect(screen.getByRole("button", { name: "Hero 템플릿" })).toBeDefined(); const templateToggle = screen.getByRole("button", { name: "템플릿" }); fireEvent.click(templateToggle); // 접힌 상태에서는 Hero 템플릿 버튼이 보이지 않아야 한다. expect(screen.queryByRole("button", { name: "Hero 템플릿" })).toBeNull(); // 다시 클릭하면 펼쳐져야 한다. fireEvent.click(templateToggle); expect(screen.getByRole("button", { name: "Hero 템플릿" })).toBeDefined(); }); it("블록 추가 버튼들은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => { render(); const textButton = screen.getByRole("button", { name: "텍스트" }); const className = textButton.getAttribute("class") ?? ""; // 라이트 모드: 밝은 배경 + 어두운 텍스트 expect(className).toContain("bg-slate-50"); expect(className).toContain("text-slate-900"); // 다크 모드: 기존 다크 스타일 유지 expect(className).toContain("dark:bg-slate-900"); expect(className).toContain("dark:text-slate-100"); }); it("히어로 템플릿 카드는 라이트/다크 테마에 맞는 카드 배경/테두리 클래스를 사용해야 한다", () => { render(); const heroDescription = screen.getByText( "페이지 상단 Hero 섹션 (큰 제목 + 서브텍스트 + 버튼)", ); const card = heroDescription.closest("div"); expect(card).not.toBeNull(); const className = card!.getAttribute("class") ?? ""; expect(className).toContain("bg-slate-50"); expect(className).toContain("border-slate-200"); expect(className).toContain("dark:border-slate-800"); expect(className).toContain("dark:bg-slate-950/50"); }); it("히어로 템플릿 추가 버튼은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => { render(); const heroButton = screen.getByRole("button", { name: "Hero 템플릿" }); const className = heroButton.getAttribute("class") ?? ""; // 라이트 모드: 밝은 배경 + 어두운 텍스트 expect(className).toContain("bg-slate-50"); expect(className).toContain("text-slate-900"); // 다크 모드: 기존 다크 톤 유지 expect(className).toContain("dark:bg-slate-900"); expect(className).toContain("dark:text-slate-100"); }); it("히어로 템플릿 썸네일 프레임은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => { render(); const thumb = screen.getByTestId("template-preview-hero") as HTMLDivElement; const className = thumb.getAttribute("class") ?? ""; // 라이트 모드: 밝은 카드 배경 + 연한 테두리 expect(className).toContain("bg-slate-100"); expect(className).toContain("border-slate-300"); // 다크 모드: 기존 다크 카드 톤 유지 expect(className).toContain("dark:bg-slate-900"); expect(className).toContain("dark:border-slate-700"); }); it("사이드바 루트는 라이트/다크 테마에 맞는 배경/테두리 클래스를 사용해야 한다", () => { const { container } = render(); const aside = container.querySelector("aside") as HTMLElement | null; expect(aside).not.toBeNull(); const className = aside!.getAttribute("class") ?? ""; expect(className).toContain("bg-white"); expect(className).toContain("border-slate-200"); expect(className).toContain("dark:border-slate-800"); expect(className).toContain("dark:bg-slate-950/40"); }); });