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");
});
});