292 lines
12 KiB
TypeScript
292 lines
12 KiB
TypeScript
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(<BlocksSidebar />);
|
|
|
|
const button = screen.getByRole("button", { name: "Hero template" });
|
|
fireEvent.click(button);
|
|
|
|
expect(templateActions.addHeroTemplateSection).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("Features 템플릿 버튼 클릭 시 addFeaturesTemplateSection 이 호출되어야 한다", () => {
|
|
render(<BlocksSidebar />);
|
|
|
|
const button = screen.getByRole("button", { name: "Features template" });
|
|
fireEvent.click(button);
|
|
|
|
expect(templateActions.addFeaturesTemplateSection).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("CTA 템플릿 버튼 클릭 시 addCtaTemplateSection 이 호출되어야 한다", () => {
|
|
render(<BlocksSidebar />);
|
|
|
|
const button = screen.getByRole("button", { name: "CTA template" });
|
|
fireEvent.click(button);
|
|
|
|
expect(templateActions.addCtaTemplateSection).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("FAQ/Pricing/Testimonials/Blog/Team/Footer 템플릿 버튼도 각각의 액션을 호출해야 한다", () => {
|
|
render(<BlocksSidebar />);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "FAQ template" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Pricing template" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Testimonials template" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Blog template" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Team template" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Footer template" }));
|
|
|
|
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(<BlocksSidebar />);
|
|
|
|
expect(screen.getByText("Top-of-page hero section (headline + subtext + button)"));
|
|
expect(screen.getByText("Three-column features section"));
|
|
expect(screen.getByText("Call-to-action (CTA) section"));
|
|
expect(screen.getByText("Frequently asked questions (FAQ) section"));
|
|
expect(screen.getByText("Pricing plans section"));
|
|
expect(screen.getByText("Customer testimonials section"));
|
|
expect(screen.getByText("Blog post listing section"));
|
|
expect(screen.getByText("Team members section"));
|
|
expect(screen.getByText("Page footer section"));
|
|
});
|
|
|
|
it("비디오 버튼 클릭 시 addVideoBlock 이 호출되어야 한다", () => {
|
|
render(<BlocksSidebar />);
|
|
|
|
const button = screen.getByRole("button", { name: "Video" });
|
|
fireEvent.click(button);
|
|
|
|
expect(otherActions.addVideoBlock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("템플릿들은 카테고리별로 그룹 헤더를 가져야 한다", () => {
|
|
render(<BlocksSidebar />);
|
|
|
|
expect(screen.getByText("Hero · CTA")).toBeDefined();
|
|
expect(screen.getByText("Content sections")).toBeDefined();
|
|
expect(screen.getByText("Trust & about")).toBeDefined();
|
|
expect(screen.getByText("Footer & misc")).toBeDefined();
|
|
});
|
|
|
|
it("각 템플릿 카드에는 미니 썸네일(레이아웃 힌트)가 렌더링되어야 한다", () => {
|
|
render(<BlocksSidebar />);
|
|
|
|
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(<BlocksSidebar />);
|
|
|
|
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(<BlocksSidebar />);
|
|
|
|
// 초기에는 텍스트 버튼이 보여야 한다.
|
|
expect(screen.getByRole("button", { name: "Text" })).toBeDefined();
|
|
|
|
const blockToggle = screen.getByRole("button", { name: "Blocks" });
|
|
fireEvent.click(blockToggle);
|
|
|
|
// 접힌 상태에서는 텍스트 버튼이 보이지 않아야 한다.
|
|
expect(screen.queryByRole("button", { name: "Text" })).toBeNull();
|
|
|
|
// 다시 클릭하면 펼쳐져야 한다.
|
|
fireEvent.click(blockToggle);
|
|
expect(screen.getByRole("button", { name: "Text" })).toBeDefined();
|
|
});
|
|
|
|
it("폼 요소 섹션 타이틀을 클릭하면 폼 관련 버튼들을 접고 펼 수 있어야 한다", () => {
|
|
render(<BlocksSidebar />);
|
|
|
|
// 초기에는 폼 입력 버튼이 보여야 한다.
|
|
expect(screen.getByRole("button", { name: "Input field" })).toBeDefined();
|
|
|
|
const formToggle = screen.getByRole("button", { name: "Form elements" });
|
|
fireEvent.click(formToggle);
|
|
|
|
// 접힌 상태에서는 폼 입력 버튼이 보이지 않아야 한다.
|
|
expect(screen.queryByRole("button", { name: "Input field" })).toBeNull();
|
|
|
|
// 다시 클릭하면 펼쳐져야 한다.
|
|
fireEvent.click(formToggle);
|
|
expect(screen.getByRole("button", { name: "Input field" })).toBeDefined();
|
|
});
|
|
|
|
it("템플릿 섹션 타이틀을 클릭하면 템플릿 카드들을 접고 펼 수 있어야 한다", () => {
|
|
render(<BlocksSidebar />);
|
|
|
|
// 초기에는 Hero 템플릿 버튼이 보여야 한다.
|
|
expect(screen.getByRole("button", { name: "Hero template" })).toBeDefined();
|
|
|
|
const templateToggle = screen.getByRole("button", { name: "Templates" });
|
|
fireEvent.click(templateToggle);
|
|
|
|
// 접힌 상태에서는 Hero 템플릿 버튼이 보이지 않아야 한다.
|
|
expect(screen.queryByRole("button", { name: "Hero template" })).toBeNull();
|
|
|
|
// 다시 클릭하면 펼쳐져야 한다.
|
|
fireEvent.click(templateToggle);
|
|
expect(screen.getByRole("button", { name: "Hero template" })).toBeDefined();
|
|
});
|
|
|
|
it("블록 추가 버튼들은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
|
render(<BlocksSidebar />);
|
|
|
|
const textButton = screen.getByRole("button", { name: "Text" });
|
|
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(<BlocksSidebar />);
|
|
|
|
const heroDescription = screen.getByText(
|
|
"Top-of-page hero section (headline + subtext + button)",
|
|
);
|
|
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(<BlocksSidebar />);
|
|
|
|
const heroButton = screen.getByRole("button", { name: "Hero template" });
|
|
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(<BlocksSidebar />);
|
|
|
|
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(<BlocksSidebar />);
|
|
|
|
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");
|
|
});
|
|
});
|