import { describe, it, expect, vi, afterEach } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import type { SectionBlockProps } from "@/features/editor/state/editorStore"; import { SectionPropertiesPanel } from "@/app/editor/panels/SectionPropertiesPanel"; // 섹션 패널의 숫자 슬라이더/프리셋 제어가 정상적으로 동작하는지 최소 한 번은 검증한다. describe("SectionPropertiesPanel", () => { const baseProps: SectionBlockProps = { background: "default", paddingY: "md", columns: [], maxWidthMode: "normal", gapX: "md", }; afterEach(() => { cleanup(); }); it("섹션 배경 색상 HEX 인풋 변경 시 updateBlock 이 backgroundColorCustom 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const hexInput = screen.getByLabelText("섹션 배경 색상 HEX 입력"); fireEvent.change(hexInput, { target: { value: "#112233" } }); expect(updateBlock).toHaveBeenCalledWith( "section-bg-1", expect.objectContaining({ backgroundColorCustom: "#112233" }), ); }); it("가로 위치 프리셋 드롭다운 변경 시 backgroundImagePositionXPercent 가 0/50/100 등으로 설정된다", () => { const updateBlock = vi.fn(); render( , ); const presetSelect = screen.getByLabelText("배경 이미지 가로 위치 프리셋"); fireEvent.change(presetSelect, { target: { value: "left" } }); expect(updateBlock).toHaveBeenCalledWith( "section-xy-x", expect.objectContaining({ backgroundImagePositionXPercent: 0 }), ); }); it("세로 위치 프리셋 드롭다운 변경 시 backgroundImagePositionYPercent 가 0/50/100 등으로 설정된다", () => { const updateBlock = vi.fn(); render( , ); const presetSelect = screen.getByLabelText("배경 이미지 세로 위치 프리셋"); fireEvent.change(presetSelect, { target: { value: "top" } }); expect(updateBlock).toHaveBeenCalledWith( "section-xy-y", expect.objectContaining({ backgroundImagePositionYPercent: 0 }), ); }); it("위치 모드를 custom 으로 두면 X/Y 퍼센트 슬라이더 변경 시 backgroundImagePositionXPercent/YPercent 가 업데이트된다", () => { const updateBlock = vi.fn(); render( , ); const xSlider = screen.getByLabelText("배경 이미지 가로 위치 슬라이더"); fireEvent.change(xSlider, { target: { value: "30" } }); expect(updateBlock).toHaveBeenCalledWith( "section-xy", expect.objectContaining({ backgroundImagePositionMode: "custom", backgroundImagePositionXPercent: 30, }), ); const ySlider = screen.getByLabelText("배경 이미지 세로 위치 슬라이더"); fireEvent.change(ySlider, { target: { value: "70" } }); expect(updateBlock).toHaveBeenCalledWith( "section-xy", expect.objectContaining({ backgroundImagePositionMode: "custom", backgroundImagePositionYPercent: 70, }), ); }); it("세로 패딩 프리셋/슬라이더 변경 시 updateBlock 이 paddingYPx 로 호출된다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("세로 패딩 슬라이더"); fireEvent.change(slider, { target: { value: "40" } }); expect(updateBlock).toHaveBeenCalledWith("section-1", expect.objectContaining({ paddingYPx: 40 })); }); it("배경 이미지 URL 인풋 변경 시 updateBlock 이 backgroundImageSrc/SourceType/AssetId 로 호출된다", () => { const updateBlock = vi.fn(); render( , ); const urlInput = screen.getByLabelText("배경 이미지 URL"); fireEvent.change(urlInput, { target: { value: "https://example.com/bg.png" } }); expect(updateBlock).toHaveBeenCalledWith( "section-2", expect.objectContaining({ backgroundImageSrc: "https://example.com/bg.png", backgroundImageSourceType: "externalUrl", backgroundImageAssetId: null, }), ); }); it("배경 이미지 크기 셀렉트 변경 시 updateBlock 이 backgroundImageSize 로 호출된다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("배경 이미지 크기"); fireEvent.change(select, { target: { value: "contain" } }); expect(updateBlock).toHaveBeenCalledWith( "section-3", expect.objectContaining({ backgroundImageSize: "contain" }), ); }); it("배경 이미지 위치 셀렉트 변경 시 updateBlock 이 backgroundImagePosition 으로 호출된다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("배경 이미지 위치"); fireEvent.change(select, { target: { value: "top" } }); expect(updateBlock).toHaveBeenCalledWith( "section-4", expect.objectContaining({ backgroundImagePosition: "top" }), ); }); it("기본 상태에서는 배경 이미지 소스가 '없음' 이고, '없음' 선택 시 관련 필드가 초기화된다", () => { const updateBlock = vi.fn(); render( , ); const sourceSelect = screen.getByLabelText("배경 이미지 소스") as HTMLSelectElement; expect(sourceSelect.value).toBe("none"); fireEvent.change(sourceSelect, { target: { value: "none" } }); expect(updateBlock).toHaveBeenCalledWith("section-0", { backgroundImageSrc: undefined, backgroundImageSourceType: undefined, backgroundImageAssetId: null, } as any); // "없음" 상태에서는 크기/위치/반복 컨트롤이 렌더되지 않아야 한다. expect(screen.queryByLabelText("배경 이미지 크기")).toBeNull(); expect(screen.queryByLabelText("배경 이미지 위치")).toBeNull(); expect(screen.queryByLabelText("배경 이미지 반복")).toBeNull(); }); it("배경 이미지 반복 셀렉트 변경 시 updateBlock 이 backgroundImageRepeat 로 호출된다", () => { const updateBlock = vi.fn(); render( , ); const repeatSelect = screen.getByLabelText("배경 이미지 반복"); fireEvent.change(repeatSelect, { target: { value: "repeat-x" } }); expect(updateBlock).toHaveBeenCalledWith( "section-6", expect.objectContaining({ backgroundImageRepeat: "repeat-x" }), ); }); it("배경 이미지 소스 셀렉트 변경 시 updateBlock 이 backgroundImageSourceType/backgroundImageAssetId 로 호출된다", () => { const updateBlock = vi.fn(); render( , ); const sourceSelect = screen.getByLabelText("배경 이미지 소스") as HTMLSelectElement; expect(sourceSelect.value).toBe("upload"); fireEvent.change(sourceSelect, { target: { value: "url" } }); expect(updateBlock).toHaveBeenCalledWith( "section-5", expect.objectContaining({ backgroundImageSourceType: "externalUrl", backgroundImageAssetId: null, }), ); }); it("배경 비디오 URL 인풋 변경 시 updateBlock 이 backgroundVideoSrc/backgroundVideoSourceType/backgroundVideoAssetId 로 호출된다", () => { const updateBlock = vi.fn(); render( , ); const urlInput = screen.getByLabelText("배경 비디오 URL"); fireEvent.change(urlInput, { target: { value: "https://example.com/bg-video.mp4" } }); expect(updateBlock).toHaveBeenCalledWith( "section-video-1", expect.objectContaining({ backgroundVideoSrc: "https://example.com/bg-video.mp4", backgroundVideoSourceType: "externalUrl", backgroundVideoAssetId: null, }), ); }); it("배경 비디오 소스 셀렉트 변경 시 updateBlock 이 backgroundVideoSourceType/backgroundVideoAssetId 로 호출된다", () => { const updateBlock = vi.fn(); render( , ); const sourceSelect = screen.getByLabelText("배경 비디오 소스") as HTMLSelectElement; expect(sourceSelect.value).toBe("upload"); fireEvent.change(sourceSelect, { target: { value: "url" } }); expect(updateBlock).toHaveBeenCalledWith( "section-video-2", expect.objectContaining({ backgroundVideoSourceType: "externalUrl", backgroundVideoAssetId: null, }), ); }); it("SectionPropertiesPanel 배경 이미지/비디오 소스 및 URL 인풋은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => { const updateBlock = vi.fn(); render( , ); const bgImageSourceSelect = screen.getByLabelText("배경 이미지 소스") as HTMLSelectElement; const bgImageSourceClass = bgImageSourceSelect.getAttribute("class") ?? ""; expect(bgImageSourceClass).toContain("bg-white"); expect(bgImageSourceClass).toContain("text-slate-900"); expect(bgImageSourceClass).toContain("border-slate-300"); expect(bgImageSourceClass).toContain("dark:bg-slate-900"); expect(bgImageSourceClass).toContain("dark:text-slate-100"); expect(bgImageSourceClass).toContain("dark:border-slate-700"); const bgImageUrlInput = screen.getByLabelText("배경 이미지 URL") as HTMLInputElement; const bgImageUrlClass = bgImageUrlInput.getAttribute("class") ?? ""; expect(bgImageUrlClass).toContain("bg-white"); expect(bgImageUrlClass).toContain("text-slate-900"); expect(bgImageUrlClass).toContain("border-slate-300"); expect(bgImageUrlClass).toContain("dark:bg-slate-900"); expect(bgImageUrlClass).toContain("dark:text-slate-100"); expect(bgImageUrlClass).toContain("dark:border-slate-700"); const bgVideoUrlInput = screen.getByLabelText("배경 비디오 URL") as HTMLInputElement; const bgVideoUrlClass = bgVideoUrlInput.getAttribute("class") ?? ""; expect(bgVideoUrlClass).toContain("bg-white"); expect(bgVideoUrlClass).toContain("text-slate-900"); expect(bgVideoUrlClass).toContain("border-slate-300"); expect(bgVideoUrlClass).toContain("dark:bg-slate-900"); expect(bgVideoUrlClass).toContain("dark:text-slate-100"); expect(bgVideoUrlClass).toContain("dark:border-slate-700"); }); it("섹션 레이아웃 및 배경 이미지 레이아웃 셀렉트들은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => { const updateBlock = vi.fn(); render( , ); const columnLayoutSelect = screen.getByLabelText("섹션 컬럼 레이아웃") as HTMLSelectElement; const alignItemsSelect = screen.getByLabelText("섹션 컬럼 세로 정렬") as HTMLSelectElement; const positionModeSelect = screen.getByLabelText("배경 이미지 위치 모드") as HTMLSelectElement; const sizeSelect = screen.getByLabelText("배경 이미지 크기") as HTMLSelectElement; const positionSelect = screen.getByLabelText("배경 이미지 위치") as HTMLSelectElement; const repeatSelect = screen.getByLabelText("배경 이미지 반복") as HTMLSelectElement; const assertDualTheme = (el: HTMLElement) => { const className = el.getAttribute("class") ?? ""; expect(className).toContain("bg-white"); expect(className).toContain("text-slate-900"); expect(className).toContain("border-slate-300"); expect(className).toContain("dark:bg-slate-900"); expect(className).toContain("dark:text-slate-100"); expect(className).toContain("dark:border-slate-700"); }; assertDualTheme(columnLayoutSelect); assertDualTheme(alignItemsSelect); assertDualTheme(positionModeSelect); assertDualTheme(sizeSelect); assertDualTheme(positionSelect); assertDualTheme(repeatSelect); }); });