Files
page-builder/tests/unit/FormFieldPropertiesPanels.spec.tsx
T
jaybe 4840a530b6
CI / test (push) Failing after 5m41s
CI / e2e (push) Has been skipped
CI / pr_and_merge (push) Has been skipped
오류 수정
2025-12-12 18:04:31 +09:00

1739 lines
56 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { FormInputPropertiesPanel } from "@/app/editor/forms/FormInputPropertiesPanel";
import { FormSelectPropertiesPanel } from "@/app/editor/forms/FormSelectPropertiesPanel";
import { FormCheckboxPropertiesPanel } from "@/app/editor/forms/FormCheckboxPropertiesPanel";
import { FormRadioPropertiesPanel } from "@/app/editor/forms/FormRadioPropertiesPanel";
import type {
Block,
FormInputBlockProps,
FormSelectBlockProps,
FormCheckboxBlockProps,
FormRadioBlockProps,
} from "@/features/editor/state/editorStore";
// Form*PropertiesPanel 컨트롤 TDD
// - 각 패널의 색상 HEX 인풋과 필드 너비 셀렉트가 updateBlock 을 올바르게 호출하는지 검증한다.
function makeBlock<T extends Block["props"]>(id: string, type: Block["type"], props: T): Block {
return { id, type, props } as any;
}
describe("FormInputPropertiesPanel", () => {
const baseProps: FormInputBlockProps = {
label: "이메일",
formFieldName: "email",
} as any;
afterEach(() => {
cleanup();
});
it("텍스트 색상 HEX 인풋 변경 시 updateBlock 이 textColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormInputBlockProps>("f-input-1", "formInput", {
...baseProps,
textColorCustom: "#ffffff",
});
render(
<FormInputPropertiesPanel
block={block}
selectedBlockId="f-input-1"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Field text color HEX");
fireEvent.change(hexInput, { target: { value: "#112233" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-input-1",
expect.objectContaining({ textColorCustom: "#112233" }),
);
});
it("채움 색상 HEX 인풋 변경 시 updateBlock 이 fillColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormInputBlockProps>("f-input-2", "formInput", {
...baseProps,
fillColorCustom: "#000000",
});
render(
<FormInputPropertiesPanel
block={block}
selectedBlockId="f-input-2"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Field fill color HEX");
fireEvent.change(hexInput, { target: { value: "#445566" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-input-2",
expect.objectContaining({ fillColorCustom: "#445566" }),
);
});
it("테두리 색상 HEX 인풋 변경 시 updateBlock 이 strokeColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormInputBlockProps>("f-input-3", "formInput", {
...baseProps,
strokeColorCustom: "#000000",
});
render(
<FormInputPropertiesPanel
block={block}
selectedBlockId="f-input-3"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Field border color HEX");
fireEvent.change(hexInput, { target: { value: "#778899" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-input-3",
expect.objectContaining({ strokeColorCustom: "#778899" }),
);
});
it("텍스트 색상 팔레트에서 \"None\" 을 선택하면 FormInput textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormInputBlockProps>("f-input-none", "formInput", {
...baseProps,
textColorCustom: "#ff0000",
} as any);
render(
<FormInputPropertiesPanel
block={block}
selectedBlockId="f-input-none"
updateBlock={updateBlock}
/>,
);
// 첫 번째 ColorPickerField(필드 텍스트 색상)의 팔레트 버튼을 연다.
const paletteButtons = screen.getAllByText("Color palette");
fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement);
// 팔레트에서 "None" 항목을 버튼 role 기준으로 선택한다.
const noneButtons = screen.getAllByRole("button", { name: "None" });
fireEvent.click(noneButtons[0]);
expect(updateBlock).toHaveBeenCalledWith(
"f-input-none",
expect.objectContaining({ textColorCustom: "" }),
);
});
it("FormInput textColorCustom 이 비어 있으면 필드 텍스트 색상 HEX 인풋 값은 빈 문자열이어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormInputBlockProps>("f-input-empty", "formInput", {
...baseProps,
textColorCustom: "",
} as any);
render(
<FormInputPropertiesPanel
block={block}
selectedBlockId="f-input-empty"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Field text color HEX") as HTMLInputElement;
expect(hexInput.value).toBe("");
});
it("필드 너비 셀렉트 변경 시 updateBlock 이 widthMode 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormInputBlockProps>("f-input-4", "formInput", {
...baseProps,
widthMode: "auto",
fullWidth: false,
});
render(
<FormInputPropertiesPanel
block={block}
selectedBlockId="f-input-4"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Width");
fireEvent.change(select, { target: { value: "fixed" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-input-4",
expect.objectContaining({ widthMode: "fixed" }),
);
});
it("FormInput 레이아웃 셀렉트는 라벨 표시 방식이 visible 일 때만 보여야 한다", () => {
const updateBlock = vi.fn();
const visibleBlock = makeBlock<FormInputBlockProps>("f-input-layout-visible", "formInput", {
...baseProps,
labelDisplay: "visible",
labelLayout: "inline",
} as any);
render(
<FormInputPropertiesPanel
block={visibleBlock}
selectedBlockId="f-input-layout-visible"
updateBlock={updateBlock}
/>,
);
const layoutSelect = screen.getByLabelText("Layout");
expect(layoutSelect).toBeTruthy();
cleanup();
const floatingBlock = makeBlock<FormInputBlockProps>("f-input-layout-floating", "formInput", {
...baseProps,
labelDisplay: "floating",
labelLayout: "inline",
} as any);
render(
<FormInputPropertiesPanel
block={floatingBlock}
selectedBlockId="f-input-layout-floating"
updateBlock={updateBlock}
/>,
);
expect(screen.queryByLabelText("Layout")).toBeNull();
});
it("폼 입력 텍스트/줄간격/자간 px 컨트롤이 fontSizeCustom/lineHeightCustom/letterSpacingCustom 으로 저장되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormInputBlockProps>("f-input-typo-1", "formInput", {
...baseProps,
fontSizeCustom: "14px",
lineHeightCustom: "20px",
letterSpacingCustom: "0px",
} as any);
render(
<FormInputPropertiesPanel
block={block}
selectedBlockId="f-input-typo-1"
updateBlock={updateBlock}
/>,
);
const fontSizeInput = screen.getByLabelText("Field text size 커스텀");
fireEvent.change(fontSizeInput, { target: { value: "24" } });
const lineHeightInput = screen.getByLabelText("Field line height 커스텀");
fireEvent.change(lineHeightInput, { target: { value: "30" } });
const letterSpacingInput = screen.getByLabelText("Field letter spacing 커스텀");
fireEvent.change(letterSpacingInput, { target: { value: "1.5" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-input-typo-1",
expect.objectContaining({ fontSizeCustom: "24px" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-input-typo-1",
expect.objectContaining({ lineHeightCustom: "30px" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-input-typo-1",
expect.objectContaining({ letterSpacingCustom: "1.5px" }),
);
});
it("폼 입력 정렬/고정 너비/패딩 컨트롤이 align/widthPx/paddingX/paddingY 로 저장되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormInputBlockProps>("f-input-layout-1", "formInput", {
...baseProps,
align: "left",
widthMode: "auto",
widthPx: 240,
paddingX: 12,
paddingY: 10,
} as any);
render(
<FormInputPropertiesPanel
block={block}
selectedBlockId="f-input-layout-1"
updateBlock={updateBlock}
/>,
);
const alignSelect = screen.getByLabelText("Text alignment");
fireEvent.change(alignSelect, { target: { value: "center" } });
const widthModeSelect = screen.getByLabelText("Width");
fireEvent.change(widthModeSelect, { target: { value: "fixed" } });
const widthPxInput = screen.getByLabelText("Field fixed width 커스텀");
fireEvent.change(widthPxInput, { target: { value: "320" } });
const paddingXInput = screen.getByLabelText("Field horizontal padding 커스텀");
fireEvent.change(paddingXInput, { target: { value: "16" } });
const paddingYInput = screen.getByLabelText("Field vertical padding 커스텀");
fireEvent.change(paddingYInput, { target: { value: "12" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-input-layout-1",
expect.objectContaining({ align: "center" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-input-layout-1",
expect.objectContaining({ widthMode: "fixed" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-input-layout-1",
expect.objectContaining({ widthPx: 320 }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-input-layout-1",
expect.objectContaining({ paddingX: 16 }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-input-layout-1",
expect.objectContaining({ paddingY: 12 }),
);
});
it("FormInput 기본 인풋/셀렉트 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormInputBlockProps>("f-input-theme", "formInput", {
...baseProps,
} as any);
render(
<FormInputPropertiesPanel
block={block}
selectedBlockId="f-input-theme"
updateBlock={updateBlock}
/>,
);
const labelTypeSelect = screen.getByLabelText("Label type") as HTMLSelectElement;
const labelTypeClass = labelTypeSelect.getAttribute("class") ?? "";
expect(labelTypeClass).toContain("bg-white");
expect(labelTypeClass).toContain("text-slate-900");
expect(labelTypeClass).toContain("border-slate-300");
expect(labelTypeClass).toContain("dark:bg-slate-900");
expect(labelTypeClass).toContain("dark:text-slate-100");
expect(labelTypeClass).toContain("dark:border-slate-700");
const labelInput = screen.getByLabelText("Field label") as HTMLInputElement;
const labelInputClass = labelInput.getAttribute("class") ?? "";
expect(labelInputClass).toContain("bg-white");
expect(labelInputClass).toContain("text-slate-900");
expect(labelInputClass).toContain("border-slate-300");
expect(labelInputClass).toContain("dark:bg-slate-900");
expect(labelInputClass).toContain("dark:text-slate-100");
expect(labelInputClass).toContain("dark:border-slate-700");
const labelDisplaySelect = screen.getByLabelText("Label display mode") as HTMLSelectElement;
const labelDisplayClass = labelDisplaySelect.getAttribute("class") ?? "";
expect(labelDisplayClass).toContain("bg-white");
expect(labelDisplayClass).toContain("text-slate-900");
expect(labelDisplayClass).toContain("border-slate-300");
expect(labelDisplayClass).toContain("dark:bg-slate-900");
expect(labelDisplayClass).toContain("dark:text-slate-100");
expect(labelDisplayClass).toContain("dark:border-slate-700");
const fieldTypeSelect = screen.getByLabelText("Field type") as HTMLSelectElement;
const fieldTypeClass = fieldTypeSelect.getAttribute("class") ?? "";
expect(fieldTypeClass).toContain("bg-white");
expect(fieldTypeClass).toContain("text-slate-900");
expect(fieldTypeClass).toContain("border-slate-300");
expect(fieldTypeClass).toContain("dark:bg-slate-900");
expect(fieldTypeClass).toContain("dark:text-slate-100");
expect(fieldTypeClass).toContain("dark:border-slate-700");
const placeholderInput = screen.getByLabelText("Placeholder") as HTMLInputElement;
const placeholderClass = placeholderInput.getAttribute("class") ?? "";
expect(placeholderClass).toContain("bg-white");
expect(placeholderClass).toContain("text-slate-900");
expect(placeholderClass).toContain("border-slate-300");
expect(placeholderClass).toContain("dark:bg-slate-900");
expect(placeholderClass).toContain("dark:text-slate-100");
expect(placeholderClass).toContain("dark:border-slate-700");
});
});
describe("FormSelectPropertiesPanel", () => {
const baseProps: FormSelectBlockProps = {
label: "카테고리",
formFieldName: "category",
options: [{ label: "A", value: "a" }],
} as any;
afterEach(() => {
cleanup();
});
it("텍스트 색상 HEX 인풋 변경 시 updateBlock 이 textColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-1", "formSelect", {
...baseProps,
textColorCustom: "#ffffff",
});
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-1"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Select text color HEX");
fireEvent.change(hexInput, { target: { value: "#112233" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-1",
expect.objectContaining({ textColorCustom: "#112233" }),
);
});
it("채움 색상 HEX 인풋 변경 시 updateBlock 이 fillColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-2", "formSelect", {
...baseProps,
fillColorCustom: "#000000",
});
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-2"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Select fill color HEX");
fireEvent.change(hexInput, { target: { value: "#445566" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-2",
expect.objectContaining({ fillColorCustom: "#445566" }),
);
});
it("테두리 색상 HEX 인풋 변경 시 updateBlock 이 strokeColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-3", "formSelect", {
...baseProps,
strokeColorCustom: "#000000",
});
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-3"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Select border color HEX");
fireEvent.change(hexInput, { target: { value: "#778899" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-3",
expect.objectContaining({ strokeColorCustom: "#778899" }),
);
});
it("필드 너비 셀렉트 변경 시 updateBlock 이 widthMode 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-4", "formSelect", {
...baseProps,
widthMode: "auto",
fullWidth: false,
});
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-4"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Field width");
fireEvent.change(select, { target: { value: "fixed" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-4",
expect.objectContaining({ widthMode: "fixed" }),
);
});
it("폼 셀렉트 텍스트/줄간격/자간 px 컨트롤이 fontSizeCustom/lineHeightCustom/letterSpacingCustom 으로 저장되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-typo-1", "formSelect", {
...baseProps,
fontSizeCustom: "14px",
lineHeightCustom: "20px",
letterSpacingCustom: "0px",
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-typo-1"
updateBlock={updateBlock}
/>,
);
const fontSizeInput = screen.getByLabelText("Select text size 커스텀");
fireEvent.change(fontSizeInput, { target: { value: "24" } });
const lineHeightInput = screen.getByLabelText("Select line height 커스텀");
fireEvent.change(lineHeightInput, { target: { value: "30" } });
const letterSpacingInput = screen.getByLabelText("Select letter spacing 커스텀");
fireEvent.change(letterSpacingInput, { target: { value: "1.5" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-typo-1",
expect.objectContaining({ fontSizeCustom: "24px" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-select-typo-1",
expect.objectContaining({ lineHeightCustom: "30px" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-select-typo-1",
expect.objectContaining({ letterSpacingCustom: "1.5px" }),
);
});
it("폼 셀렉트 너비/패딩 컨트롤이 widthPx/paddingX/paddingY 로 저장되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-layout-1", "formSelect", {
...baseProps,
widthMode: "fixed",
widthPx: 240,
paddingX: 12,
paddingY: 10,
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-layout-1"
updateBlock={updateBlock}
/>,
);
const widthPxInput = screen.getByLabelText("Field fixed width 커스텀");
fireEvent.change(widthPxInput, { target: { value: "320" } });
const paddingXInput = screen.getByLabelText("Select horizontal padding 커스텀");
fireEvent.change(paddingXInput, { target: { value: "16" } });
const paddingYInput = screen.getByLabelText("Select vertical padding 커스텀");
fireEvent.change(paddingYInput, { target: { value: "12" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-layout-1",
expect.objectContaining({ widthPx: 320 }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-select-layout-1",
expect.objectContaining({ paddingX: 16 }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-select-layout-1",
expect.objectContaining({ paddingY: 12 }),
);
});
it("FormSelect 레이아웃 셀렉트는 라벨 표시 방식이 visible 일 때만 보여야 한다", () => {
const updateBlock = vi.fn();
const visibleBlock = makeBlock<FormSelectBlockProps>("f-select-layout-visible", "formSelect", {
...baseProps,
labelDisplay: "visible",
labelLayout: "inline",
} as any);
render(
<FormSelectPropertiesPanel
block={visibleBlock}
selectedBlockId="f-select-layout-visible"
updateBlock={updateBlock}
/>,
);
const layoutSelect = screen.getByLabelText("Layout");
expect(layoutSelect).toBeTruthy();
cleanup();
const hiddenBlock = makeBlock<FormSelectBlockProps>("f-select-layout-hidden", "formSelect", {
...baseProps,
labelDisplay: "hidden",
labelLayout: "inline",
} as any);
render(
<FormSelectPropertiesPanel
block={hiddenBlock}
selectedBlockId="f-select-layout-hidden"
updateBlock={updateBlock}
/>,
);
expect(screen.queryByLabelText("Layout")).toBeNull();
});
it("라벨 타입 셀렉트 변경 시 updateBlock 이 labelMode 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-label-mode", "formSelect", {
...baseProps,
labelMode: "text",
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-label-mode"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Label type");
fireEvent.change(select, { target: { value: "image" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-label-mode",
expect.objectContaining({ labelMode: "image" }),
);
});
it("라벨 표시 방식 셀렉트 변경 시 updateBlock 이 labelDisplay 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-label-display", "formSelect", {
...baseProps,
labelDisplay: "visible",
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-label-display"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Label display mode");
fireEvent.change(select, { target: { value: "hidden" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-label-display",
expect.objectContaining({ labelDisplay: "hidden" }),
);
});
it("레이아웃 셀렉트 변경 시 updateBlock 이 labelLayout 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-label-layout", "formSelect", {
...baseProps,
labelDisplay: "visible",
labelLayout: "stacked",
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-label-layout"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Layout");
fireEvent.change(select, { target: { value: "inline" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-label-layout",
expect.objectContaining({ labelLayout: "inline" }),
);
});
it("라벨/필드 간격 컨트롤 변경 시 updateBlock 이 labelGapPx 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-label-gap", "formSelect", {
...baseProps,
labelDisplay: "visible",
labelLayout: "inline",
labelGapPx: 8,
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-label-gap"
updateBlock={updateBlock}
/>,
);
const gapInput = screen.getByLabelText("Label/field gap 커스텀");
fireEvent.change(gapInput, { target: { value: "16" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-label-gap",
expect.objectContaining({ labelGapPx: 16 }),
);
});
it("필드 라벨 인풋 변경 시 updateBlock 이 label 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-label", "formSelect", {
...baseProps,
label: "카테고리",
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-label"
updateBlock={updateBlock}
/>,
);
const input = screen.getByLabelText("Field label");
fireEvent.change(input, { target: { value: "새 카테고리" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-label",
expect.objectContaining({ label: "새 카테고리" }),
);
});
it("전송 키 인풋 변경 시 updateBlock 이 formFieldName 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-name", "formSelect", {
...baseProps,
formFieldName: "category",
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-name"
updateBlock={updateBlock}
/>,
);
const input = screen.getByLabelText("Submit key");
fireEvent.change(input, { target: { value: "category_new" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-name",
expect.objectContaining({ formFieldName: "category_new" }),
);
});
it("필드 모서리 둥글기 컨트롤 변경 시 updateBlock 이 borderRadius 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-radius", "formSelect", {
...baseProps,
borderRadius: "md",
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-radius"
updateBlock={updateBlock}
/>,
);
const radiusInput = screen.getByLabelText("Field border radius 커스텀");
fireEvent.change(radiusInput, { target: { value: "8" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-select-radius",
expect.objectContaining({ borderRadius: "full" }),
);
});
it("FormSelect 기본 인풋/셀렉트/옵션 편집 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-theme", "formSelect", {
...baseProps,
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-theme"
updateBlock={updateBlock}
/>,
);
const labelTypeSelect = screen.getByLabelText("Label type") as HTMLSelectElement;
const labelTypeClass = labelTypeSelect.getAttribute("class") ?? "";
expect(labelTypeClass).toContain("bg-white");
expect(labelTypeClass).toContain("text-slate-900");
expect(labelTypeClass).toContain("border-slate-300");
expect(labelTypeClass).toContain("dark:bg-slate-900");
expect(labelTypeClass).toContain("dark:text-slate-100");
expect(labelTypeClass).toContain("dark:border-slate-700");
const labelInput = screen.getByLabelText("Field label") as HTMLInputElement;
const labelInputClass = labelInput.getAttribute("class") ?? "";
expect(labelInputClass).toContain("bg-white");
expect(labelInputClass).toContain("text-slate-900");
expect(labelInputClass).toContain("border-slate-300");
expect(labelInputClass).toContain("dark:bg-slate-900");
expect(labelInputClass).toContain("dark:text-slate-100");
expect(labelInputClass).toContain("dark:border-slate-700");
const labelDisplaySelect = screen.getByLabelText("Label display mode") as HTMLSelectElement;
const labelDisplayClass = labelDisplaySelect.getAttribute("class") ?? "";
expect(labelDisplayClass).toContain("bg-white");
expect(labelDisplayClass).toContain("text-slate-900");
expect(labelDisplayClass).toContain("border-slate-300");
expect(labelDisplayClass).toContain("dark:bg-slate-900");
expect(labelDisplayClass).toContain("dark:text-slate-100");
expect(labelDisplayClass).toContain("dark:border-slate-700");
const widthModeSelect = screen.getByLabelText("Field width") as HTMLSelectElement;
const widthModeClass = widthModeSelect.getAttribute("class") ?? "";
expect(widthModeClass).toContain("bg-white");
expect(widthModeClass).toContain("text-slate-900");
expect(widthModeClass).toContain("border-slate-300");
expect(widthModeClass).toContain("dark:bg-slate-900");
expect(widthModeClass).toContain("dark:text-slate-100");
expect(widthModeClass).toContain("dark:border-slate-700");
const optionLabelInput = screen.getByPlaceholderText("Label") as HTMLInputElement;
const optionLabelClass = optionLabelInput.getAttribute("class") ?? "";
expect(optionLabelClass).toContain("bg-white");
expect(optionLabelClass).toContain("text-slate-900");
expect(optionLabelClass).toContain("border-slate-300");
expect(optionLabelClass).toContain("dark:bg-slate-900");
expect(optionLabelClass).toContain("dark:text-slate-100");
expect(optionLabelClass).toContain("dark:border-slate-700");
});
it("FormSelect 옵션 행은 라벨/값 인풋과 삭제 버튼을 위한 3열 그리드 레이아웃 클래스를 사용해야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormSelectBlockProps>("f-select-option-layout", "formSelect", {
...baseProps,
} as any);
render(
<FormSelectPropertiesPanel
block={block}
selectedBlockId="f-select-option-layout"
updateBlock={updateBlock}
/>,
);
const optionLabelInput = screen.getByPlaceholderText("Label") as HTMLInputElement;
const rowDiv = optionLabelInput.closest("div") as HTMLDivElement | null;
expect(rowDiv).not.toBeNull();
const rowClass = rowDiv!.getAttribute("class") ?? "";
expect(rowClass).toContain("grid");
expect(rowClass).toContain("grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]");
});
});
describe("FormCheckboxPropertiesPanel", () => {
const baseProps: FormCheckboxBlockProps = {
groupLabel: "옵션들",
formFieldName: "features",
options: [{ label: "옵션 1", value: "opt1" }],
} as any;
afterEach(() => {
cleanup();
});
it("텍스트 색상 HEX 인풋 변경 시 updateBlock 이 textColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-1", "formCheckbox", {
...baseProps,
textColorCustom: "#ffffff",
});
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-1"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Checkbox text color HEX");
fireEvent.change(hexInput, { target: { value: "#112233" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-1",
expect.objectContaining({ textColorCustom: "#112233" }),
);
});
it("폼 체크박스 텍스트/줄간격/자간 px 컨트롤이 fontSizeCustom/lineHeightCustom/letterSpacingCustom 으로 저장되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-typo-1", "formCheckbox", {
...baseProps,
fontSizeCustom: "14px",
lineHeightCustom: "20px",
letterSpacingCustom: "0px",
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-typo-1"
updateBlock={updateBlock}
/>,
);
const fontSizeInput = screen.getByLabelText("Checkbox text size 커스텀");
fireEvent.change(fontSizeInput, { target: { value: "24" } });
const lineHeightInput = screen.getByLabelText("Checkbox line height 커스텀");
fireEvent.change(lineHeightInput, { target: { value: "30" } });
const letterSpacingInput = screen.getByLabelText("Checkbox letter spacing 커스텀");
fireEvent.change(letterSpacingInput, { target: { value: "1.5" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-typo-1",
expect.objectContaining({ fontSizeCustom: "24px" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-check-typo-1",
expect.objectContaining({ lineHeightCustom: "30px" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-check-typo-1",
expect.objectContaining({ letterSpacingCustom: "1.5px" }),
);
});
it("폼 체크박스 너비/패딩 컨트롤이 widthPx/paddingX/paddingY 로 저장되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-layout-1", "formCheckbox", {
...baseProps,
widthMode: "fixed",
widthPx: 240,
paddingX: 12,
paddingY: 10,
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-layout-1"
updateBlock={updateBlock}
/>,
);
const widthPxInput = screen.getByLabelText("Field fixed width 커스텀");
fireEvent.change(widthPxInput, { target: { value: "320" } });
const paddingXInput = screen.getByLabelText("Checkbox horizontal padding 커스텀");
fireEvent.change(paddingXInput, { target: { value: "16" } });
const paddingYInput = screen.getByLabelText("Checkbox vertical padding 커스텀");
fireEvent.change(paddingYInput, { target: { value: "12" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-layout-1",
expect.objectContaining({ widthPx: 320 }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-check-layout-1",
expect.objectContaining({ paddingX: 16 }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-check-layout-1",
expect.objectContaining({ paddingY: 12 }),
);
});
it("FormCheckbox 레이아웃 셀렉트는 그룹 라벨 표시 방식이 visible 일 때만 보여야 한다", () => {
const updateBlock = vi.fn();
const visibleBlock = makeBlock<FormCheckboxBlockProps>("f-check-layout-visible", "formCheckbox", {
...baseProps,
groupLabelDisplay: "visible",
labelLayout: "inline",
} as any);
render(
<FormCheckboxPropertiesPanel
block={visibleBlock}
selectedBlockId="f-check-layout-visible"
updateBlock={updateBlock}
/>,
);
const layoutSelect = screen.getByLabelText("Layout");
expect(layoutSelect).toBeTruthy();
cleanup();
const hiddenBlock = makeBlock<FormCheckboxBlockProps>("f-check-layout-hidden", "formCheckbox", {
...baseProps,
groupLabelDisplay: "hidden",
labelLayout: "inline",
} as any);
render(
<FormCheckboxPropertiesPanel
block={hiddenBlock}
selectedBlockId="f-check-layout-hidden"
updateBlock={updateBlock}
/>,
);
expect(screen.queryByLabelText("Layout")).toBeNull();
});
it("그룹 타이틀 타입 셀렉트 변경 시 updateBlock 이 groupLabelMode 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-group-mode", "formCheckbox", {
...baseProps,
groupLabelMode: "text",
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-group-mode"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Group title type");
fireEvent.change(select, { target: { value: "image" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-group-mode",
expect.objectContaining({ groupLabelMode: "image" }),
);
});
it("그룹 타이틀 표시 방식 셀렉트 변경 시 updateBlock 이 groupLabelDisplay 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-group-display", "formCheckbox", {
...baseProps,
groupLabelDisplay: "visible",
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-group-display"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Group title display mode");
fireEvent.change(select, { target: { value: "hidden" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-group-display",
expect.objectContaining({ groupLabelDisplay: "hidden" }),
);
});
it("레이아웃 셀렉트 변경 시 updateBlock 이 labelLayout 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-label-layout", "formCheckbox", {
...baseProps,
groupLabelDisplay: "visible",
labelLayout: "stacked",
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-label-layout"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Layout");
fireEvent.change(select, { target: { value: "inline" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-label-layout",
expect.objectContaining({ labelLayout: "inline" }),
);
});
it("라벨/필드 간격 컨트롤 변경 시 updateBlock 이 labelGapPx 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-label-gap", "formCheckbox", {
...baseProps,
groupLabelDisplay: "visible",
labelLayout: "inline",
labelGapPx: 8,
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-label-gap"
updateBlock={updateBlock}
/>,
);
const gapInput = screen.getByLabelText("Label/field gap 커스텀");
fireEvent.change(gapInput, { target: { value: "16" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-label-gap",
expect.objectContaining({ labelGapPx: 16 }),
);
});
it("그룹 타이틀 인풋 변경 시 updateBlock 이 groupLabel 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-group-label", "formCheckbox", {
...baseProps,
groupLabel: "옵션들",
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-group-label"
updateBlock={updateBlock}
/>,
);
const input = screen.getByLabelText("Group title");
fireEvent.change(input, { target: { value: "새 옵션들" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-group-label",
expect.objectContaining({ groupLabel: "새 옵션들" }),
);
});
it("체크박스 옵션 간격 컨트롤 변경 시 updateBlock 이 optionGapPx 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-option-gap", "formCheckbox", {
...baseProps,
optionGapPx: 4,
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-option-gap"
updateBlock={updateBlock}
/>,
);
const gapInput = screen.getByLabelText("Checkbox option gap 커스텀");
fireEvent.change(gapInput, { target: { value: "10" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-option-gap",
expect.objectContaining({ optionGapPx: 10 }),
);
});
it("체크박스 옵션 레이아웃 셀렉트 변경 시 updateBlock 이 optionLayout 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-option-layout", "formCheckbox", {
...baseProps,
optionLayout: "stacked",
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-option-layout"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Option layout");
fireEvent.change(select, { target: { value: "inline" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-option-layout",
expect.objectContaining({ optionLayout: "inline" }),
);
});
it("체크박스 필드 모서리 둥글기 컨트롤 변경 시 updateBlock 이 borderRadius 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-radius", "formCheckbox", {
...baseProps,
borderRadius: "md",
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-radius"
updateBlock={updateBlock}
/>,
);
const radiusInput = screen.getByLabelText("Field border radius 커스텀");
fireEvent.change(radiusInput, { target: { value: "8" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-check-radius",
expect.objectContaining({ borderRadius: "full" }),
);
});
it("FormCheckbox 필드 너비 셀렉트는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormCheckboxBlockProps>("f-check-theme-width", "formCheckbox", {
...baseProps,
} as any);
render(
<FormCheckboxPropertiesPanel
block={block}
selectedBlockId="f-check-theme-width"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Field width") 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("폼 라디오 텍스트/줄간격/자간 px 컨트롤이 fontSizeCustom/lineHeightCustom/letterSpacingCustom 으로 저장되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-typo-1", "formRadio", {
...baseProps,
fontSizeCustom: "14px",
lineHeightCustom: "20px",
letterSpacingCustom: "0px",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-typo-1"
updateBlock={updateBlock}
/>,
);
const fontSizeInput = screen.getByLabelText("Radio text size 커스텀");
fireEvent.change(fontSizeInput, { target: { value: "24" } });
const lineHeightInput = screen.getByLabelText("Radio line height 커스텀");
fireEvent.change(lineHeightInput, { target: { value: "30" } });
const letterSpacingInput = screen.getByLabelText("Radio letter spacing 커스텀");
fireEvent.change(letterSpacingInput, { target: { value: "1.5" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-typo-1",
expect.objectContaining({ fontSizeCustom: "24px" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-typo-1",
expect.objectContaining({ lineHeightCustom: "30px" }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-typo-1",
expect.objectContaining({ letterSpacingCustom: "1.5px" }),
);
});
it("폼 라디오 너비/패딩/옵션 간격 컨트롤이 widthPx/paddingX/paddingY/optionGapPx 로 저장되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-layout-1", "formRadio", {
...baseProps,
widthMode: "fixed",
widthPx: 240,
paddingX: 8,
paddingY: 4,
optionGapPx: 8,
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-layout-1"
updateBlock={updateBlock}
/>,
);
const widthPxInput = screen.getByLabelText("Field fixed width 커스텀");
fireEvent.change(widthPxInput, { target: { value: "320" } });
const paddingXInput = screen.getByLabelText("Radio horizontal padding 커스텀");
fireEvent.change(paddingXInput, { target: { value: "12" } });
const paddingYInput = screen.getByLabelText("Radio vertical padding 커스텀");
fireEvent.change(paddingYInput, { target: { value: "6" } });
const optionGapInput = screen.getByLabelText("Radio option gap 커스텀");
fireEvent.change(optionGapInput, { target: { value: "10" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-layout-1",
expect.objectContaining({ widthPx: 320 }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-layout-1",
expect.objectContaining({ paddingX: 12 }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-layout-1",
expect.objectContaining({ paddingY: 6 }),
);
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-layout-1",
expect.objectContaining({ optionGapPx: 10 }),
);
});
it("FormRadio 그룹 타이틀 레이아웃 셀렉트는 그룹 타이틀 표시 방식이 visible 일 때만 보여야 한다", () => {
const updateBlock = vi.fn();
const visibleBlock = makeBlock<FormRadioBlockProps>("f-radio-layout-visible", "formRadio", {
...baseProps,
groupLabelDisplay: "visible",
labelLayout: "inline",
} as any);
render(
<FormRadioPropertiesPanel
block={visibleBlock}
selectedBlockId="f-radio-layout-visible"
updateBlock={updateBlock}
/>,
);
const layoutSelect = screen.getByLabelText("Group title layout");
expect(layoutSelect).toBeTruthy();
cleanup();
const hiddenBlock = makeBlock<FormRadioBlockProps>("f-radio-layout-hidden", "formRadio", {
...baseProps,
groupLabelDisplay: "hidden",
labelLayout: "inline",
} as any);
render(
<FormRadioPropertiesPanel
block={hiddenBlock}
selectedBlockId="f-radio-layout-hidden"
updateBlock={updateBlock}
/>,
);
expect(screen.queryByLabelText("Group title layout")).toBeNull();
});
it("그룹 타이틀 타입 셀렉트 변경 시 updateBlock 이 groupLabelMode 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-group-mode", "formRadio", {
...baseProps,
groupLabelMode: "text",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-group-mode"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Group title type");
fireEvent.change(select, { target: { value: "image" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-group-mode",
expect.objectContaining({ groupLabelMode: "image" }),
);
});
it("그룹 타이틀 표시 방식 셀렉트 변경 시 updateBlock 이 groupLabelDisplay 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-group-display", "formRadio", {
...baseProps,
groupLabelDisplay: "visible",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-group-display"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Group title display mode");
fireEvent.change(select, { target: { value: "hidden" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-group-display",
expect.objectContaining({ groupLabelDisplay: "hidden" }),
);
});
it("그룹 타이틀 레이아웃 셀렉트 변경 시 updateBlock 이 labelLayout 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-label-layout", "formRadio", {
...baseProps,
groupLabelDisplay: "visible",
labelLayout: "stacked",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-label-layout"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Group title layout");
fireEvent.change(select, { target: { value: "inline" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-label-layout",
expect.objectContaining({ labelLayout: "inline" }),
);
});
it("라벨/필드 간격 컨트롤 변경 시 updateBlock 이 labelGapPx 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-label-gap", "formRadio", {
...baseProps,
groupLabelDisplay: "visible",
labelLayout: "inline",
labelGapPx: 8,
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-label-gap"
updateBlock={updateBlock}
/>,
);
const gapInput = screen.getByLabelText("Label/field gap 커스텀");
fireEvent.change(gapInput, { target: { value: "16" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-label-gap",
expect.objectContaining({ labelGapPx: 16 }),
);
});
it("그룹 타이틀 인풋 변경 시 updateBlock 이 groupLabel 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-group-label", "formRadio", {
...baseProps,
groupLabel: "플랜",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-group-label"
updateBlock={updateBlock}
/>,
);
const input = screen.getByLabelText("Group title");
fireEvent.change(input, { target: { value: "새 플랜" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-group-label",
expect.objectContaining({ groupLabel: "새 플랜" }),
);
});
it("그룹 타이틀 이미지 소스 셀렉트 변경 시 updateBlock 이 groupLabelImageSource 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-group-image-source", "formRadio", {
...baseProps,
groupLabelMode: "image",
groupLabelImageSource: "url",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-group-image-source"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Group title image source");
fireEvent.change(select, { target: { value: "upload" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-group-image-source",
expect.objectContaining({ groupLabelImageSource: "upload" }),
);
});
it("그룹 타이틀 이미지 URL 인풋 변경 시 updateBlock 이 groupLabelImageUrl 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-group-image-url", "formRadio", {
...baseProps,
groupLabelMode: "image",
groupLabelImageSource: "url",
groupLabelImageUrl: "https://example.com/old.png",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-group-image-url"
updateBlock={updateBlock}
/>,
);
const input = screen.getByLabelText("Group title image URL");
fireEvent.change(input, { target: { value: "https://example.com/new.png" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-group-image-url",
expect.objectContaining({ groupLabelImageUrl: "https://example.com/new.png" }),
);
});
it("옵션 이미지 소스 셀렉트 변경 시 updateBlock 이 optionImageSource 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-option-image-source", "formRadio", {
...baseProps,
optionImageSource: "url",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-option-image-source"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("Option image source");
fireEvent.change(select, { target: { value: "upload" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-option-image-source",
expect.objectContaining({ optionImageSource: "upload" }),
);
});
it("라디오 배경 색상 HEX 인풋 변경 시 updateBlock 이 fillColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-fill-color", "formRadio", {
...baseProps,
fillColorCustom: "#000000",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-fill-color"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Radio background color HEX");
fireEvent.change(hexInput, { target: { value: "#445566" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-fill-color",
expect.objectContaining({ fillColorCustom: "#445566" }),
);
});
it("라디오 테두리 색상 HEX 인풋 변경 시 updateBlock 이 strokeColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-stroke-color", "formRadio", {
...baseProps,
strokeColorCustom: "#000000",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-stroke-color"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("Radio border color HEX");
fireEvent.change(hexInput, { target: { value: "#778899" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-stroke-color",
expect.objectContaining({ strokeColorCustom: "#778899" }),
);
});
it("라디오 필드 모서리 둥글기 컨트롤 변경 시 updateBlock 이 borderRadius 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-radius", "formRadio", {
...baseProps,
borderRadius: "md",
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-radius"
updateBlock={updateBlock}
/>,
);
const radiusInput = screen.getByLabelText("Field border radius 커스텀");
fireEvent.change(radiusInput, { target: { value: "8" } });
expect(updateBlock).toHaveBeenCalledWith(
"f-radio-radius",
expect.objectContaining({ borderRadius: "full" }),
);
});
it("FormRadio 기본 인풋/셀렉트/옵션 편집 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
const updateBlock = vi.fn();
const block = makeBlock<FormRadioBlockProps>("f-radio-theme", "formRadio", {
...baseProps,
} as any);
render(
<FormRadioPropertiesPanel
block={block}
selectedBlockId="f-radio-theme"
updateBlock={updateBlock}
/>,
);
const groupTitleInput = screen.getByLabelText("Group title") as HTMLInputElement;
const groupTitleClass = groupTitleInput.getAttribute("class") ?? "";
expect(groupTitleClass).toContain("bg-white");
expect(groupTitleClass).toContain("text-slate-900");
expect(groupTitleClass).toContain("border-slate-300");
expect(groupTitleClass).toContain("dark:bg-slate-900");
expect(groupTitleClass).toContain("dark:text-slate-100");
expect(groupTitleClass).toContain("dark:border-slate-700");
const groupDisplaySelect = screen.getByLabelText("Group title display mode") as HTMLSelectElement;
const groupDisplayClass = groupDisplaySelect.getAttribute("class") ?? "";
expect(groupDisplayClass).toContain("bg-white");
expect(groupDisplayClass).toContain("text-slate-900");
expect(groupDisplayClass).toContain("border-slate-300");
expect(groupDisplayClass).toContain("dark:bg-slate-900");
expect(groupDisplayClass).toContain("dark:text-slate-100");
expect(groupDisplayClass).toContain("dark:border-slate-700");
const optionLayoutSelect = screen.getByLabelText("Option layout") as HTMLSelectElement;
const optionLayoutClass = optionLayoutSelect.getAttribute("class") ?? "";
expect(optionLayoutClass).toContain("bg-white");
expect(optionLayoutClass).toContain("text-slate-900");
expect(optionLayoutClass).toContain("border-slate-300");
expect(optionLayoutClass).toContain("dark:bg-slate-900");
expect(optionLayoutClass).toContain("dark:text-slate-100");
expect(optionLayoutClass).toContain("dark:border-slate-700");
const formFieldNameInput = screen.getByLabelText("Submit key") as HTMLInputElement;
const formFieldNameClass = formFieldNameInput.getAttribute("class") ?? "";
expect(formFieldNameClass).toContain("bg-white");
expect(formFieldNameClass).toContain("text-slate-900");
expect(formFieldNameClass).toContain("border-slate-300");
expect(formFieldNameClass).toContain("dark:bg-slate-900");
expect(formFieldNameClass).toContain("dark:text-slate-100");
expect(formFieldNameClass).toContain("dark:border-slate-700");
const optionLabelInput = screen.getByPlaceholderText("Label") as HTMLInputElement;
const optionLabelClass = optionLabelInput.getAttribute("class") ?? "";
expect(optionLabelClass).toContain("bg-white");
expect(optionLabelClass).toContain("text-slate-900");
expect(optionLabelClass).toContain("border-slate-300");
expect(optionLabelClass).toContain("dark:bg-slate-900");
expect(optionLabelClass).toContain("dark:text-slate-100");
expect(optionLabelClass).toContain("dark:border-slate-700");
});
});