import { describe, it, expect, vi, afterEach } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import { ListPropertiesPanel } from "@/app/editor/panels/ListPropertiesPanel"; import type { ListBlockProps } from "@/features/editor/state/editorStore"; describe("ListPropertiesPanel", () => { const baseProps: ListBlockProps = { items: ["아이템 1", "아이템 2"], ordered: false, align: "left", }; afterEach(() => { cleanup(); }); it("리스트 아이템 textarea 변경 시 items 와 itemsTree 로 updateBlock 이 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const textarea = screen.getByLabelText("List items"); fireEvent.change(textarea, { target: { value: "첫째\n둘째" } }); expect(updateBlock).toHaveBeenCalledWith( "list-1", expect.objectContaining({ items: ["첫째", "둘째"], }), ); }); it("리스트 정렬 셀렉트 변경 시 updateBlock 이 align 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("List alignment"); fireEvent.change(select, { target: { value: "center" } }); expect(updateBlock).toHaveBeenCalledWith( "list-align", expect.objectContaining({ align: "center" }), ); }); it("글자 크기 슬라이더 변경 시 updateBlock 이 fontSizeCustom 을 px 문자열로 호출해야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("Font size 슬라이더"); fireEvent.change(slider, { target: { value: "20" } }); expect(updateBlock).toHaveBeenCalledWith( "list-font-size", expect.objectContaining({ fontSizeCustom: "20px" }), ); }); it("줄 간격 슬라이더 변경 시 updateBlock 이 lineHeightCustom 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("Line height 슬라이더"); fireEvent.change(slider, { target: { value: "1.8" } }); expect(updateBlock).toHaveBeenCalledWith( "list-line-height", expect.objectContaining({ lineHeightCustom: "1.8" }), ); }); it("텍스트 색상 HEX 인풋 변경 시 updateBlock 이 textColorCustom 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const hexInput = screen.getByLabelText("List text color HEX"); fireEvent.change(hexInput, { target: { value: "#112233" } }); expect(updateBlock).toHaveBeenCalledWith( "list-text-color", expect.objectContaining({ textColorCustom: "#112233" }), ); }); it("텍스트 색상 팔레트에서 \"None\" 을 선택하면 textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => { const updateBlock = vi.fn(); render( , ); // 첫 번째 ColorPickerField(텍스트 색상)의 팔레트 버튼을 연다. const paletteButtons = screen.getAllByText("Color palette"); fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement); // 팔레트에서 "None" 항목을 버튼 role 기준으로 선택한다 (불릿 스타일 select 의 option "none" 과 구분). const noneButtons = screen.getAllByRole("button", { name: "None" }); fireEvent.click(noneButtons[0]); expect(updateBlock).toHaveBeenCalledWith( "list-text-none", expect.objectContaining({ textColorCustom: "" }), ); }); it("textColorCustom 이 비어 있으면 텍스트 색상 HEX 인풋 값은 빈 문자열이어야 한다", () => { const updateBlock = vi.fn(); render( , ); const hexInput = screen.getByLabelText("List text color HEX") as HTMLInputElement; expect(hexInput.value).toBe(""); }); it("불릿 스타일 셀렉트 변경 시 bulletStyle 과 ordered 가 함께 업데이트되어야 한다 (decimal)", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("List bullet style"); fireEvent.change(select, { target: { value: "decimal" } }); expect(updateBlock).toHaveBeenCalledWith( "list-bullet-decimal", expect.objectContaining({ bulletStyle: "decimal", ordered: true }), ); }); it("불릿 스타일 셀렉트에서 none 선택 시 ordered 가 false 로 설정되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("List bullet style"); fireEvent.change(select, { target: { value: "none" } }); expect(updateBlock).toHaveBeenCalledWith( "list-bullet-none", expect.objectContaining({ bulletStyle: "none", ordered: false }), ); }); it("아이템 간 여백 슬라이더 변경 시 updateBlock 이 gapYPx 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("Item gap 슬라이더"); fireEvent.change(slider, { target: { value: "16" } }); expect(updateBlock).toHaveBeenCalledWith( "list-gapY", expect.objectContaining({ gapYPx: 16 }), ); }); it("리스트 정렬 셀렉트는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("List alignment") as HTMLSelectElement; const className = select.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"); }); it("리스트 불릿 스타일 셀렉트는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("List bullet style") as HTMLSelectElement; const className = select.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"); }); it("리스트 아이템 textarea 는 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => { const updateBlock = vi.fn(); render( , ); const textarea = screen.getByLabelText("List items") 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"); }); });