import { describe, it, expect, afterEach } from "vitest"; import { render, screen, cleanup } from "@testing-library/react"; import { PublicPageRenderer } from "@/features/editor/components/PublicPageRenderer"; import type { Block, SectionBlockProps, TextBlockProps } from "@/features/editor/state/editorStore"; // PublicPageRenderer 섹션/루트 레이아웃 TDD // - Export 레이어의 pb-root / pb-section-* 구조와 개념적으로 대응되는 프리뷰 레이아웃을 고정한다. // - 정확한 px 값 비교 대신, 주요 Tailwind 레이아웃 클래스 존재 여부를 검증한다. describe("PublicPageRenderer - 섹션/루트 레이아웃", () => { afterEach(() => { cleanup(); }); const makeSectionWithText = (sectionOverride: Partial = {}, textOverride: Partial = {}): Block[] => { const sectionProps: SectionBlockProps = { background: "default", paddingY: "md", columns: [{ id: "sec_layout_col_1", span: 12 }], maxWidthMode: "normal", gapX: "md", alignItems: "top", ...sectionOverride, } as SectionBlockProps; const textProps: TextBlockProps = { text: "섹션 레이아웃 텍스트", align: "left", size: "base", ...textOverride, } as TextBlockProps; const section: Block = { id: "sec_layout_1", type: "section", props: sectionProps, } as any; const text: Block = { id: "sec_layout_1_text", type: "text", sectionId: "sec_layout_1", columnId: "sec_layout_col_1", props: textProps, } as any; return [section, text]; }; it("섹션 레이아웃은 mx-auto / max-w / px-* 및 flex 컬럼/갭 구조로 렌더되어야 한다", () => { const blocks: Block[] = makeSectionWithText(); render(); const inner = screen.getByTestId("preview-section-inner") as HTMLDivElement; const columns = screen.getByTestId("preview-section-columns") as HTMLDivElement; // Export 의 pb-section-inner / pb-section-columns 에 대응되는 레이아웃 구조를 최소한으로 보장한다. expect(inner.className).toContain("mx-auto"); expect(inner.className).toContain("px-4"); expect(inner.className).toMatch(/max-w-/); expect(columns.className).toContain("flex"); // gapX 기본값(md)에 대해 gap-8 이 사용되는 현재 구현을 고정한다. expect(columns.className).toContain("gap-8"); // 컬럼 래퍼는 flex-col 과 일정한 세로 간격을 가진다. const columnWrapper = columns.querySelector("div.flex.flex-col") as HTMLDivElement | null; expect(columnWrapper).not.toBeNull(); if (columnWrapper) { expect(columnWrapper.className).toContain("gap-4"); } // 섹션 텍스트가 실제로 섹션 안에 렌더되는지 확인한다. const textEl = screen.getByText("섹션 레이아웃 텍스트"); const section = textEl.closest("section"); expect(section).not.toBeNull(); }); it("루트 블록은 상단 섹션 내 max-width 컨테이너 안에서 렌더되어야 한다", () => { const rootTextProps: TextBlockProps = { text: "루트 레이아웃 텍스트", align: "left", size: "base", } as TextBlockProps; const rootTextBlock: Block = { id: "root_layout_text", type: "text", props: rootTextProps, } as any; const sectionBlocks = makeSectionWithText(); const blocks: Block[] = [rootTextBlock, ...sectionBlocks]; render(); const rootTextEl = screen.getByText("루트 레이아웃 텍스트") as HTMLParagraphElement; // 루트 텍스트는 상단 섹션(py-12) 안에 존재해야 한다. const rootSection = rootTextEl.closest("section"); expect(rootSection).not.toBeNull(); if (!rootSection) return; expect(rootSection.className).toContain("py-12"); // 섹션 바로 아래의 max-width 컨테이너 구조를 확인한다. const innerContainer = rootSection.querySelector("div"); expect(innerContainer).not.toBeNull(); if (!innerContainer) return; const className = innerContainer.className; expect(className).toContain("mx-auto"); expect(className).toContain("max-w-3xl"); expect(className).toContain("px-4"); expect(className).toContain("flex"); expect(className).toContain("gap-4"); }); });