import { describe, it, expect, vi, afterEach } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import { ButtonPropertiesPanel } from "@/app/editor/panels/ButtonPropertiesPanel"; import type { ButtonBlockProps } from "@/features/editor/state/editorStore"; // ButtonPropertiesPanel 컨트롤 TDD // - 색상 HEX 인풋 및 너비 모드 셀렉트가 updateBlock 을 올바르게 호출하는지 검증한다. describe("ButtonPropertiesPanel", () => { const baseProps: ButtonBlockProps = { label: "버튼", href: "#", align: "left", size: "md", variant: "solid", colorPalette: "primary", }; afterEach(() => { cleanup(); }); it("텍스트 색상 HEX 인풋 변경 시 updateBlock 이 textColorCustom 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const hexInput = screen.getByLabelText("버튼 텍스트 색상 HEX"); fireEvent.change(hexInput, { target: { value: "#112233" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-1", expect.objectContaining({ textColorCustom: "#112233" }), ); }); it("채움 색상 HEX 인풋 변경 시 updateBlock 이 fillColorCustom 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const hexInput = screen.getByLabelText("버튼 채움 색상 HEX"); fireEvent.change(hexInput, { target: { value: "#445566" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-2", expect.objectContaining({ fillColorCustom: "#445566" }), ); }); it("외곽선 색상 HEX 인풋 변경 시 updateBlock 이 strokeColorCustom 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const hexInput = screen.getByLabelText("버튼 외곽선 색상 HEX"); fireEvent.change(hexInput, { target: { value: "#778899" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-3", expect.objectContaining({ strokeColorCustom: "#778899" }), ); }); it("텍스트 색상 팔레트에서 \"없음\" 을 선택하면 textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => { const updateBlock = vi.fn(); render( , ); // 첫 번째 ColorPickerField(텍스트 색상)의 팔레트 버튼을 연다. const paletteButtons = screen.getAllByText("색상 팔레트"); fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement); // 팔레트에서 "없음" 항목을 버튼 role 기준으로 선택한다. const noneButtons = screen.getAllByRole("button", { name: "없음" }); fireEvent.click(noneButtons[0]); expect(updateBlock).toHaveBeenCalledWith( "btn-text-none", expect.objectContaining({ textColorCustom: "" }), ); }); it("textColorCustom 이 비어 있으면 버튼 텍스트 색상 HEX 인풋 값은 빈 문자열이어야 한다", () => { const updateBlock = vi.fn(); render( , ); const hexInput = screen.getByLabelText("버튼 텍스트 색상 HEX") as HTMLInputElement; expect(hexInput.value).toBe(""); }); it("버튼 너비 모드 셀렉트 변경 시 updateBlock 이 widthMode 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("버튼 너비 모드"); fireEvent.change(select, { target: { value: "fixed" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-4", expect.objectContaining({ widthMode: "fixed" }), ); }); it("버튼 이미지 URL 인풋 변경 시 updateBlock 이 imageSrc 와 imageSourceType(externalUrl) 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const urlInput = screen.getByLabelText("버튼 이미지 URL"); fireEvent.change(urlInput, { target: { value: "https://example.com/new.png" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-img-url", expect.objectContaining({ imageSrc: "https://example.com/new.png", imageSourceType: "externalUrl", }), ); }); it("버튼 이미지 위치 셀렉트 변경 시 updateBlock 이 imagePlacement 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("버튼 이미지 위치"); fireEvent.change(select, { target: { value: "right" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-img-pos", expect.objectContaining({ imagePlacement: "right" }), ); }); it("버튼 이미지 소스 셀렉트에서 URL/업로드를 전환할 수 있어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("버튼 이미지 소스"); // 업로드 → URL 로 전환 fireEvent.change(select, { target: { value: "url" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-img-source", expect.objectContaining({ imageSourceType: "externalUrl", imageAssetId: null, }), ); }); it("버튼 텍스트 변경 시 updateBlock 이 label 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const textarea = screen.getByLabelText("버튼 텍스트"); fireEvent.change(textarea, { target: { value: "새 버튼" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-label", expect.objectContaining({ label: "새 버튼" }), ); }); it("버튼 텍스트 textarea 는 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => { const updateBlock = vi.fn(); render( , ); const textarea = screen.getByLabelText("버튼 텍스트") as HTMLTextAreaElement; const className = textarea.getAttribute("class") ?? ""; // 라이트 모드: 흰 배경 + 어두운 텍스트 expect(className).toContain("bg-white"); expect(className).toContain("text-slate-900"); // 다크 모드: 다크 배경 + 밝은 텍스트 expect(className).toContain("dark:bg-slate-900"); expect(className).toContain("dark:text-slate-100"); }); it("가로 패딩 슬라이더 변경 시 updateBlock 이 paddingX 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("가로 패딩 (px) 슬라이더"); fireEvent.change(slider, { target: { value: "24" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-padding-x", expect.objectContaining({ paddingX: 24 }), ); }); it("세로 패딩 슬라이더 변경 시 updateBlock 이 paddingY 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("세로 패딩 (px) 슬라이더"); fireEvent.change(slider, { target: { value: "18" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-padding-y", expect.objectContaining({ paddingY: 18 }), ); }); it("버튼 링크 인풋 변경 시 updateBlock 이 href 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const input = screen.getByLabelText("버튼 링크"); fireEvent.change(input, { target: { value: "/signup" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-href", expect.objectContaining({ href: "/signup" }), ); }); it("버튼 정렬 셀렉트 변경 시 updateBlock 이 align 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("버튼 정렬"); fireEvent.change(select, { target: { value: "center" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-align", expect.objectContaining({ align: "center" }), ); }); it("버튼 스타일 셀렉트 변경 시 updateBlock 이 variant 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("버튼 스타일"); fireEvent.change(select, { target: { value: "outline" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-variant", expect.objectContaining({ variant: "outline" }), ); }); it("모서리 둥글기 슬라이더 변경 시 updateBlock 이 borderRadius 토큰으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("모서리 둥글기 슬라이더"); fireEvent.change(slider, { target: { value: "4" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-radius", expect.objectContaining({ borderRadius: "full" }), ); }); it("widthMode 가 fixed 일 때 고정 너비 슬라이더 변경 시 updateBlock 이 widthPx 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("버튼 고정 너비 슬라이더"); fireEvent.change(slider, { target: { value: "300" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-width-fixed", expect.objectContaining({ widthPx: 300 }), ); }); it("버튼 크기 슬라이더 변경 시 updateBlock 이 fontSizeCustom 을 px 문자열로 호출해야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("버튼 크기 슬라이더"); fireEvent.change(slider, { target: { value: "20" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-font-size", expect.objectContaining({ fontSizeCustom: "20px" }), ); }); it("줄 간격 슬라이더 변경 시 updateBlock 이 lineHeightCustom 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("줄 간격 슬라이더"); fireEvent.change(slider, { target: { value: "1.8" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-line-height", expect.objectContaining({ lineHeightCustom: "1.8" }), ); }); it("글자 간격 슬라이더 변경 시 updateBlock 이 letterSpacingCustom 을 em 단위로 호출해야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("글자 간격 슬라이더"); fireEvent.change(slider, { target: { value: "16" } }); expect(updateBlock).toHaveBeenCalledWith( "btn-letter-spacing", expect.objectContaining({ letterSpacingCustom: "1em" }), ); }); it("버튼 주요 인풋/셀렉트 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => { const updateBlock = vi.fn(); render( , ); const labelTextarea = screen.getByLabelText("버튼 텍스트") as HTMLTextAreaElement; const labelClass = labelTextarea.getAttribute("class") ?? ""; expect(labelClass).toContain("bg-white"); expect(labelClass).toContain("text-slate-900"); expect(labelClass).toContain("border-slate-300"); expect(labelClass).toContain("dark:bg-slate-900"); expect(labelClass).toContain("dark:text-slate-100"); expect(labelClass).toContain("dark:border-slate-700"); const imageSourceSelect = screen.getByLabelText("버튼 이미지 소스") as HTMLSelectElement; const imageSourceClass = imageSourceSelect.getAttribute("class") ?? ""; expect(imageSourceClass).toContain("bg-white"); expect(imageSourceClass).toContain("text-slate-900"); expect(imageSourceClass).toContain("border-slate-300"); expect(imageSourceClass).toContain("dark:bg-slate-900"); expect(imageSourceClass).toContain("dark:text-slate-100"); expect(imageSourceClass).toContain("dark:border-slate-700"); const alignSelect = screen.getByLabelText("버튼 정렬") as HTMLSelectElement; const alignClass = alignSelect.getAttribute("class") ?? ""; expect(alignClass).toContain("bg-white"); expect(alignClass).toContain("text-slate-900"); expect(alignClass).toContain("border-slate-300"); expect(alignClass).toContain("dark:bg-slate-900"); expect(alignClass).toContain("dark:text-slate-100"); expect(alignClass).toContain("dark:border-slate-700"); }); });