Files
page-builder/tests/unit/PublicPageRendererFormFieldStyles.spec.tsx
T
jaybe 676e58cad7
CI / test (push) Failing after 5m22s
CI / e2e (push) Has been skipped
CI / pr_and_merge (push) Has been skipped
테마 적용
2025-12-09 18:53:21 +09:00

842 lines
29 KiB
TypeScript

import { describe, it, expect, afterEach } from "vitest";
import { render, cleanup } from "@testing-library/react";
import { PublicPageRenderer } from "@/features/editor/components/PublicPageRenderer";
import type { Block } from "@/features/editor/state/editorStore";
// 폼 필드 스타일 TDD
// - formInput: 색상/패딩/너비/둥글기 스타일
// - formSelect: 색상/패딩/너비/둥글기 스타일
// - formCheckbox/formRadio: 옵션 컨테이너 색상/패딩/둥글기 스타일
function getFirstLabel(root: HTMLElement): HTMLLabelElement {
const label = root.querySelector("label");
if (!label) {
throw new Error("label element not found");
}
return label as HTMLLabelElement;
}
describe("PublicPageRenderer - 폼 필드 스타일", () => {
afterEach(() => {
cleanup();
});
it("formInput 블록은 색상/패딩/너비/둥글기 스타일을 반영해야 한다", () => {
const blocks: Block[] = [
{
id: "form_input_styles",
type: "formInput",
props: {
label: "이메일",
formFieldName: "email",
align: "center",
widthMode: "fixed",
widthPx: 320,
paddingX: 16,
paddingY: 8,
textColorCustom: "#112233",
fillColorCustom: "#123456",
strokeColorCustom: "#ff0000",
borderRadius: "lg",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const wrapper = getByTestId("preview-form-input-wrapper") as HTMLElement;
const input = wrapper.querySelector('[data-testid="preview-form-input"]') as HTMLElement | null;
expect(input).not.toBeNull();
// 정렬
expect((input as HTMLInputElement).style.textAlign).toBe("center");
// width: 320px / 16 = 20em
expect((input as HTMLInputElement).style.width).toBe("20em");
// padding-inline/block: 16px/8px -> 1em / 0.5em
expect((input as HTMLInputElement).style.paddingInline).toBe("1em");
expect((input as HTMLInputElement).style.paddingBlock).toBe("0.5em");
// 색상: hex -> rgb 변환
expect((input as HTMLInputElement).style.color).toBe("rgb(17, 34, 51)");
expect((input as HTMLInputElement).style.backgroundColor).toBe("rgb(18, 52, 86)");
expect((input as HTMLInputElement).style.borderColor).toBe("rgb(255, 0, 0)");
// borderRadius: lg -> 6px
expect((input as HTMLInputElement).style.borderRadius).toBe("6px");
});
it("formInput 블록에서 inputType/email 과 formFieldName 은 type/name/placeholder 로 반영되어야 한다", () => {
const blocks: Block[] = [
{
id: "form_input_email_attrs",
type: "formInput",
props: {
label: "이메일",
formFieldName: "email_field",
inputType: "email",
placeholder: "이메일 주소",
} as any,
},
];
const { getByLabelText } = render(<PublicPageRenderer blocks={blocks} />);
const input = getByLabelText("이메일") as HTMLInputElement;
expect(input.type).toBe("email");
expect(input.name).toBe("email_field");
expect(input.placeholder).toBe("이메일 주소");
});
it("formInput 블록의 타이포그래피 커스텀 값은 em 단위 스타일로 반영되어야 한다", () => {
const blocks: Block[] = [
{
id: "form_input_typography",
type: "formInput",
props: {
label: "커스텀 타이포",
formFieldName: "typo",
fontSizeCustom: "24px",
lineHeightCustom: "30px",
letterSpacingCustom: "2px",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const wrapper = getByTestId("preview-form-input-wrapper") as HTMLElement;
const input = wrapper.querySelector('[data-testid="preview-form-input"]') as HTMLInputElement | null;
expect(input).not.toBeNull();
expect(input!.style.fontSize).toBe("1.5em");
expect(input!.style.lineHeight).toBe("1.875em");
expect(input!.style.letterSpacing).toBe("0.125em");
});
it("formInput 기본 래퍼 클래스에는 text-slate-* 색상 클래스가 없어야 한다", () => {
const blocks: Block[] = [
{
id: "form_input_neutral_wrapper",
type: "formInput",
props: {
label: "이름",
formFieldName: "name",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const wrapper = getByTestId("preview-form-input-wrapper") as HTMLElement;
// 기본 래퍼에는 text-slate-* 색상 클래스를 사용하지 않고, 텍스트 색상은 상위/브라우저 기본값을 따른다.
expect(wrapper.className).not.toContain("text-slate-");
});
it("formInput 블록에서 라벨 표시 방식이 hidden 이면 sr-only 클래스로 시각적으로 숨겨야 한다", () => {
const blocks: Block[] = [
{
id: "form_input_hidden_label",
type: "formInput",
props: {
label: "숨김 라벨",
formFieldName: "hidden_label",
labelDisplay: "hidden",
} as any,
},
];
const { getByTestId, getByLabelText } = render(<PublicPageRenderer blocks={blocks} />);
const wrapper = getByTestId("preview-form-input-wrapper") as HTMLElement;
const span = wrapper.querySelector("span");
expect(span).not.toBeNull();
expect((span as HTMLSpanElement).className).toContain("sr-only");
// label 이 숨김이더라도 접근성 측면에서 getByLabelText 로는 접근 가능해야 한다.
const input = getByLabelText("숨김 라벨") as HTMLInputElement;
expect(input).toBeTruthy();
});
it("formInput 블록에서 라벨 표시 방식이 floating 이면 pb-form-field/pb-form-label 기반 플로팅 라벨 마크업을 사용해야 한다", () => {
const blocks: Block[] = [
{
id: "form_input_floating_label",
type: "formInput",
props: {
label: "플로팅 라벨",
formFieldName: "floating_label",
labelDisplay: "floating",
} as any,
},
];
const { container, getByLabelText } = render(<PublicPageRenderer blocks={blocks} />);
const field = container.querySelector(
'[data-testid="preview-form-input-wrapper"]',
) as HTMLDivElement | null;
expect(field).not.toBeNull();
expect(field!.className).toContain("pb-form-field");
expect(field!.className).toContain("pb-form-field--floating");
const firstChild = field!.firstElementChild as HTMLElement | null;
const lastChild = field!.lastElementChild as HTMLElement | null;
expect(firstChild).not.toBeNull();
expect(lastChild).not.toBeNull();
// 플로팅 라벨 CSS 는 input/textarea 다음 형제 label 을 기준으로 동작하므로,
// DOM 상에서 input 이 먼저, label 이 나중에 와야 한다.
expect(firstChild!.getAttribute("data-testid")).toBe("preview-form-input");
expect(lastChild!.tagName).toBe("LABEL");
const label = lastChild as HTMLLabelElement;
expect(label.className).toContain("pb-form-label");
const input = getByLabelText("플로팅 라벨") as HTMLInputElement;
expect(input.className).toContain("pb-input");
// 플로팅 라벨 모드에서는 placeholder 를 공백으로 두고, 라벨만 인풋 안/위에서 이동시킨다.
expect(input.placeholder).toBe(" ");
// formFieldName 은 name/id/htmlFor 로 연결되어야 한다.
expect(input.name).toBe("floating_label");
expect(input.id).toBe("floating_label");
expect(label.htmlFor).toBe("floating_label");
});
it("formInput 플로팅 라벨에서 paddingY 값은 pb-form-field--floating 의 --pb-input-padding-y CSS 변수로 전달되어야 한다", () => {
const blocks: Block[] = [
{
id: "form_input_floating_padding",
type: "formInput",
props: {
label: "플로팅 패딩",
formFieldName: "floating_padding",
labelDisplay: "floating",
paddingY: 16,
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const field = getByTestId("preview-form-input-wrapper") as HTMLDivElement;
// paddingY: 16px -> 1rem 으로 변환되어 CSS 변수에 설정되어야 한다.
expect(field.style.getPropertyValue("--pb-input-padding-y")).toBe("1rem");
});
it("formInput 플로팅 라벨에서 strokeColorCustom 은 wrapper 의 --pb-input-border-color CSS 변수로 전달되어야 한다", () => {
const blocks: Block[] = [
{
id: "form_input_floating_border_color",
type: "formInput",
props: {
label: "플로팅 보더",
formFieldName: "floating_border",
labelDisplay: "floating",
strokeColorCustom: "#ff0000",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const field = getByTestId("preview-form-input-wrapper") as HTMLDivElement;
// CSS 변수는 원본 hex 문자열 그대로 설정된다.
expect(field.style.getPropertyValue("--pb-input-border-color")).toBe("#ff0000");
});
it("formInput 플로팅 라벨에서 label 과 placeholder 가 같으면 placeholder 텍스트를 인풋 안에 표시하지 않아야 한다", () => {
const blocks: Block[] = [
{
id: "form_input_floating_same_placeholder",
type: "formInput",
props: {
label: "이름",
formFieldName: "name",
labelDisplay: "floating",
placeholder: "이름",
} as any,
},
];
const { getByLabelText } = render(<PublicPageRenderer blocks={blocks} />);
const input = getByLabelText("이름") as HTMLInputElement;
// 플로팅 라벨 모드에서는 placeholder 를 공백으로 두고, 라벨만 인풋 안/위에서 이동시킨다.
expect(input.placeholder).toBe(" ");
});
it("formSelect 블록은 색상/패딩/너비/둥글기 스타일을 반영해야 한다", () => {
const blocks: Block[] = [
{
id: "form_select_styles",
type: "formSelect",
props: {
label: "카테고리",
formFieldName: "category",
options: [
{ label: "A", value: "a" },
{ label: "B", value: "b" },
],
widthMode: "fixed",
widthPx: 240,
paddingX: 16,
paddingY: 8,
textColorCustom: "#00ff00",
fillColorCustom: "#123456",
strokeColorCustom: "#ff0000",
borderRadius: "lg",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const wrapper = getByTestId("preview-form-select") as HTMLElement;
const select = wrapper.querySelector("select") as HTMLSelectElement | null;
expect(select).not.toBeNull();
// width: 240px / 16 = 15em
expect(wrapper.style.width).toBe("15em");
// padding-inline/block: 16px/8px -> 1em / 0.5em
expect(select!.style.paddingInline).toBe("1em");
expect(select!.style.paddingBlock).toBe("0.5em");
// 색상
expect(select!.style.color).toBe("rgb(0, 255, 0)");
expect(select!.style.backgroundColor).toBe("rgb(18, 52, 86)");
expect(select!.style.borderColor).toBe("rgb(255, 0, 0)");
// 둥글기
expect(select!.style.borderRadius).toBe("6px");
});
it("formSelect 블록에서 라벨 표시 방식이 hidden 이면 sr-only 클래스로 시각적으로 숨겨야 한다", () => {
const blocks: Block[] = [
{
id: "form_select_hidden_label",
type: "formSelect",
props: {
label: "숨김 셀렉트 라벨",
formFieldName: "select_hidden",
options: [
{ label: "A", value: "a" },
],
labelDisplay: "hidden",
} as any,
},
];
const { container } = render(<PublicPageRenderer blocks={blocks} />);
const label = container.querySelector("label") as HTMLLabelElement | null;
expect(label).not.toBeNull();
const span = label!.querySelector("span");
expect(span).not.toBeNull();
expect((span as HTMLSpanElement).className).toContain("sr-only");
});
it("formSelect 블록의 select 요소는 formFieldName 을 name 속성으로 사용해야 한다", () => {
const blocks: Block[] = [
{
id: "form_select_name_attr",
type: "formSelect",
props: {
label: "카테고리",
formFieldName: "category_field",
options: [
{ label: "A", value: "a" },
{ label: "B", value: "b" },
],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const wrapper = getByTestId("preview-form-select") as HTMLElement;
const select = wrapper.querySelector("select") as HTMLSelectElement | null;
expect(select).not.toBeNull();
expect(select!.name).toBe("category_field");
});
it("formCheckbox 블록은 옵션 컨테이너에 색상/패딩/둥글기 스타일을 반영해야 한다", () => {
const blocks: Block[] = [
{
id: "form_checkbox_styles",
type: "formCheckbox",
props: {
groupLabel: "옵션들",
formFieldName: "features",
options: [{ label: "옵션 1", value: "opt1" }],
widthMode: "fixed",
widthPx: 320,
paddingX: 8,
paddingY: 4,
optionGapPx: 16,
textColorCustom: "#ffffff",
fillColorCustom: "#123456",
strokeColorCustom: "#ff0000",
borderRadius: "lg",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-checkbox-group") as HTMLElement;
const firstLabel = getFirstLabel(group);
// width: 320px / 16 = 20em
expect(group.style.width).toBe("20em");
// 옵션 간 간격: 16px -> 1em
const optionsContainer = group.querySelector('[data-testid="preview-form-checkbox-options"]') as HTMLElement | null;
expect(optionsContainer).not.toBeNull();
expect(optionsContainer!.style.rowGap).toBe("1em");
// 옵션 컨테이너 스타일: padding, 배경, 보더, 둥글기
expect(firstLabel.style.paddingInline).toBe("0.5em");
expect(firstLabel.style.paddingBlock).toBe("0.25em");
expect(firstLabel.style.backgroundColor).toBe("rgb(18, 52, 86)");
expect(firstLabel.style.borderColor).toBe("rgb(255, 0, 0)");
expect(firstLabel.style.borderRadius).toBe("6px");
});
it("formCheckbox 그룹 래퍼 클래스에는 text-slate-* 색상 클래스가 없어야 한다", () => {
const blocks: Block[] = [
{
id: "form_checkbox_neutral_wrapper",
type: "formCheckbox",
props: {
groupLabel: "옵션들",
formFieldName: "features",
options: [{ label: "옵션 1", value: "opt1" }],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-checkbox-group") as HTMLElement;
// 체크박스 그룹 래퍼에는 text-slate-* 색상 클래스를 사용하지 않고, 텍스트 색상은 토큰/브라우저 기본값을 따른다.
expect(group.className).not.toContain("text-slate-");
});
it("formRadio 그룹 래퍼 클래스에는 text-slate-* 색상 클래스가 없어야 한다", () => {
const blocks: Block[] = [
{
id: "form_radio_neutral_wrapper",
type: "formRadio",
props: {
groupLabel: "라디오 그룹",
formFieldName: "radio_group",
options: [{ label: "옵션 A", value: "a" }],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-radio-group") as HTMLElement;
// 라디오 그룹 래퍼에는 text-slate-* 색상 클래스를 사용하지 않고, 텍스트 색상은 토큰/브라우저 기본값을 따른다.
expect(group.className).not.toContain("text-slate-");
});
it("formRadio 블록의 옵션 input 은 type/name/value 및 FormBlock.requiredFieldIds 기반 required 를 가져야 한다", () => {
const blocks: Block[] = [
{
id: "form_ctrl_radio",
type: "form",
props: {
kind: "contact",
submitTarget: "internal",
requiredFieldIds: ["radio_required"],
} as any,
},
{
id: "radio_required",
type: "formRadio",
props: {
groupLabel: "플랜",
formFieldName: "plan",
options: [
{ label: "A", value: "a" },
{ label: "B", value: "b" },
],
} as any,
},
];
type: "form",
props: {
kind: "contact",
submitTarget: "internal",
requiredFieldIds: ["radio_required"],
} as any,
},
{
id: "radio_required",
type: "formRadio",
props: {
groupLabel: "플랜",
formFieldName: "plan",
options: [
{ label: "A", value: "a" },
{ label: "B", value: "b" },
],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-radio-group") as HTMLElement;
const inputs = group.querySelectorAll("input[type='radio']");
expect(inputs.length).toBe(2);
const first = inputs[0] as HTMLInputElement;
const second = inputs[1] as HTMLInputElement;
expect(first.type).toBe("radio");
expect(first.name).toBe("plan");
expect(first.value).toBe("a");
expect(first.required).toBe(true);
expect(second.type).toBe("radio");
expect(second.name).toBe("plan");
expect(second.value).toBe("b");
expect(second.required).toBe(false);
});
it("formCheckbox 블록의 각 옵션 라벨은 builder.css 의 pb-form-option 클래스를 사용해야 한다", () => {
const blocks: Block[] = [
{
id: "form_checkbox_pb_option_class",
type: "formCheckbox",
props: {
groupLabel: "옵션들",
formFieldName: "features",
options: [
{ label: "옵션 1", value: "opt1" },
{ label: "옵션 2", value: "opt2" },
],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-checkbox-group") as HTMLElement;
const optionLabels = group.querySelectorAll("label");
expect(optionLabels.length).toBeGreaterThan(0);
optionLabels.forEach((label) => {
expect((label as HTMLLabelElement).className).toContain("pb-form-option");
});
});
it("formRadio 프리뷰의 옵션 input 은 OS 기본 스타일을 사용하기 위해 색상 관련 Tailwind 클래스(bg-/text-/border-)를 사용하지 않아야 한다", () => {
const blocks: Block[] = [
{
id: "form_radio_os_default",
type: "formRadio",
props: {
groupLabel: "라디오",
formFieldName: "radio_os",
options: [{ label: "옵션 A", value: "a" }],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-radio-group") as HTMLElement;
const input = group.querySelector("input[type='radio']") as HTMLInputElement | null;
expect(input).not.toBeNull();
// 크기/모양(h-4,w-4 등)만 유지하고, 색상 관련 Tailwind 유틸은 사용하지 않는다.
expect(input!.className).not.toContain("bg-");
expect(input!.className).not.toContain("text-");
expect(input!.className).not.toContain("border-");
});
it("formCheckbox 블록에서 optionLayout 이 stacked 이면 옵션 컨테이너가 pb-form-options--stacked 클래스를 사용해야 한다", () => {
const blocks: Block[] = [
{
id: "form_checkbox_layout_stacked",
type: "formCheckbox",
props: {
groupLabel: "옵션들",
formFieldName: "features",
options: [
{ label: "옵션 1", value: "opt1" },
{ label: "옵션 2", value: "opt2" },
],
optionLayout: "stacked",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const optionsContainer = getByTestId(
"preview-form-checkbox-options",
) as HTMLElement;
expect(optionsContainer.className).toContain("pb-form-options");
expect(optionsContainer.className).toContain("pb-form-options--stacked");
});
it("formCheckbox 블록에서 optionLayout 이 inline 이면 옵션 컨테이너가 pb-form-options--inline 클래스를 사용해야 한다", () => {
const blocks: Block[] = [
{
id: "form_checkbox_layout_inline",
type: "formCheckbox",
props: {
groupLabel: "옵션들",
formFieldName: "features",
options: [
{ label: "옵션 1", value: "opt1" },
{ label: "옵션 2", value: "opt2" },
],
optionLayout: "inline",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const optionsContainer = getByTestId(
"preview-form-checkbox-options",
) as HTMLElement;
expect(optionsContainer.className).toContain("pb-form-options");
expect(optionsContainer.className).toContain("pb-form-options--inline");
});
it("formCheckbox 블록에서 그룹 타이틀 표시 방식이 hidden 이면 sr-only 클래스로 시각적으로 숨겨야 한다", () => {
const blocks: Block[] = [
{
id: "form_checkbox_hidden_group_label",
type: "formCheckbox",
props: {
groupLabel: "숨김 체크박스 그룹",
formFieldName: "features_hidden",
options: [{ label: "옵션 1", value: "opt1" }],
groupLabelDisplay: "hidden",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-checkbox-group") as HTMLElement;
const spans = group.querySelectorAll("span");
const spanArray = Array.from(spans) as HTMLSpanElement[];
const groupLabelSpan = spanArray.find((s) => s.textContent === "숨김 체크박스 그룹");
expect(groupLabelSpan).toBeTruthy();
expect(groupLabelSpan!.className).toContain("sr-only");
});
it("formCheckbox 블록의 옵션 input 은 type/ name/value 및 FormBlock.requiredFieldIds 기반 required 를 가져야 한다", () => {
const blocks: Block[] = [
{
id: "form_ctrl_checkbox",
type: "form",
props: {
kind: "contact",
submitTarget: "internal",
requiredFieldIds: ["checkbox_required"],
} as any,
},
{
id: "checkbox_required",
type: "formCheckbox",
props: {
groupLabel: "기능",
formFieldName: "feature",
options: [
{ label: "A", value: "a" },
{ label: "B", value: "b" },
],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-checkbox-group") as HTMLElement;
const inputs = group.querySelectorAll("input[type='checkbox']");
expect(inputs.length).toBe(2);
const first = inputs[0] as HTMLInputElement;
const second = inputs[1] as HTMLInputElement;
expect(first.type).toBe("checkbox");
expect(first.name).toBe("feature");
expect(first.value).toBe("a");
expect(first.required).toBe(true);
expect(second.type).toBe("checkbox");
expect(second.name).toBe("feature");
expect(second.value).toBe("b");
expect(second.required).toBe(false);
});
it("formCheckbox 프리뷰의 옵션 input 은 OS 기본 스타일을 사용하기 위해 색상 관련 Tailwind 클래스(bg-/text-/border-)를 사용하지 않아야 한다", () => {
const blocks: Block[] = [
{
id: "form_checkbox_os_default",
type: "formCheckbox",
props: {
groupLabel: "체크",
formFieldName: "features_os",
options: [{ label: "옵션 1", value: "opt1" }],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-checkbox-group") as HTMLElement;
const input = group.querySelector("input[type='checkbox']") as HTMLInputElement | null;
expect(input).not.toBeNull();
// 크기/모양(h-4,w-4,rounded 등)만 유지하고, 색상 관련 Tailwind 유틸은 사용하지 않는다.
expect(input!.className).not.toContain("bg-");
expect(input!.className).not.toContain("text-");
expect(input!.className).not.toContain("border-");
});
it("formCheckbox 프리뷰의 옵션 input 은 OS 기본 스타일을 사용하기 위해 색상 관련 Tailwind 클래스(bg-/text-/border-)를 사용하지 않아야 한다", () => {
const blocks: Block[] = [
{
id: "form_checkbox_os_default",
type: "formCheckbox",
props: {
groupLabel: "체크",
formFieldName: "features_os",
options: [{ label: "옵션 1", value: "opt1" }],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-checkbox-group") as HTMLElement;
const input = group.querySelector("input[type='checkbox']") as HTMLInputElement | null;
expect(input).not.toBeNull();
// 크기/모양(h-4,w-4,rounded 등)만 유지하고, 색상 관련 Tailwind 유틸은 사용하지 않는다.
expect(input!.className).not.toContain("bg-");
expect(input!.className).not.toContain("text-");
expect(input!.className).not.toContain("border-");
});
it("formRadio 블록에서 그룹 타이틀 레이아웃이 inline 이면 그룹 컨테이너가 가로 정렬되고 세로 중앙 정렬 및 columnGap 이 labelGapPx 로 반영되어야 한다", () => {
const blocks: Block[] = [
{
id: "form_radio_label_inline_preview",
type: "formRadio",
props: {
groupLabel: "플랜 인라인",
formFieldName: "plan-inline",
groupLabelDisplay: "visible",
labelLayout: "inline",
labelGapPx: 16,
options: [
{ label: "플랜 A", value: "a" },
{ label: "플랜 B", value: "b" },
],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-radio-group") as HTMLElement;
expect(group.className).toContain("flex");
expect(group.className).toContain("flex-row");
expect(group.className).toContain("items-center");
// labelGapPx: 16px → 1em
expect(group.style.columnGap).toBe("1em");
});
it("formCheckbox 블록에서 그룹 타이틀 레이아웃이 inline 이면 그룹 컨테이너가 가로 정렬되고 세로 중앙 정렬 및 columnGap 이 labelGapPx 로 반영되어야 한다", () => {
const blocks: Block[] = [
{
id: "form_checkbox_label_inline_preview",
type: "formCheckbox",
props: {
groupLabel: "체크 인라인",
formFieldName: "features-inline",
groupLabelDisplay: "visible",
labelLayout: "inline",
labelGapPx: 12,
options: [
{ label: "옵션 1", value: "opt1" },
{ label: "옵션 2", value: "opt2" },
],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-checkbox-group") as HTMLElement;
expect(group.className).toContain("flex");
expect(group.className).toContain("flex-row");
expect(group.className).toContain("items-center");
// labelGapPx: 12px → 0.75em
expect(group.style.columnGap).toBe("0.75em");
});
it("formRadio 블록은 옵션 컨테이너에 색상/패딩/둥글기 스타일을 반영해야 한다", () => {
const blocks: Block[] = [
{
id: "form_radio_styles",
type: "formRadio",
props: {
groupLabel: "플랜",
formFieldName: "plan",
options: [{ label: "플랜 A", value: "a" }],
widthMode: "fixed",
widthPx: 320,
paddingX: 8,
paddingY: 4,
optionGapPx: 16,
textColorCustom: "#ffffff",
fillColorCustom: "#123456",
strokeColorCustom: "#ff0000",
borderRadius: "lg",
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-radio-group") as HTMLElement;
const firstLabel = getFirstLabel(group);
// width: 320px / 16 = 20em
expect(group.style.width).toBe("20em");
// 옵션 간 간격: 16px -> 1em
const optionsContainer = group.querySelector('[data-testid="preview-form-radio-options"]') as HTMLElement | null;
expect(optionsContainer).not.toBeNull();
expect(optionsContainer!.style.rowGap).toBe("1em");
// 옵션 컨테이너 스타일: padding, 배경, 보더, 둥글기
expect(firstLabel.style.paddingInline).toBe("0.5em");
expect(firstLabel.style.paddingBlock).toBe("0.25em");
expect(firstLabel.style.backgroundColor).toBe("rgb(18, 52, 86)");
expect(firstLabel.style.borderColor).toBe("rgb(255, 0, 0)");
expect(firstLabel.style.borderRadius).toBe("6px");
});
it("formRadio 블록의 각 옵션 라벨은 builder.css 의 pb-form-option 클래스를 사용해야 한다", () => {
const blocks: Block[] = [
{
id: "form_radio_pb_option_class",
type: "formRadio",
props: {
groupLabel: "플랜",
formFieldName: "plan",
options: [
{ label: "플랜 A", value: "a" },
{ label: "플랜 B", value: "b" },
],
} as any,
},
];
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const group = getByTestId("preview-form-radio-group") as HTMLElement;
const optionLabels = group.querySelectorAll("label");
expect(optionLabels.length).toBeGreaterThan(0);
optionLabels.forEach((label) => {
expect((label as HTMLLabelElement).className).toContain("pb-form-option");
});
});
});