TDD,E2E 개선, 미적용 스타일 개선
This commit is contained in:
@@ -63,6 +63,165 @@ describe("PublicPageRenderer - 폼 필드 스타일", () => {
|
||||
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 블록에서 라벨 표시 방식이 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 플로팅 라벨에서 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[] = [
|
||||
{
|
||||
@@ -106,6 +265,55 @@ describe("PublicPageRenderer - 폼 필드 스타일", () => {
|
||||
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[] = [
|
||||
{
|
||||
@@ -148,6 +356,255 @@ describe("PublicPageRenderer - 폼 필드 스타일", () => {
|
||||
expect(firstLabel.style.borderRadius).toBe("6px");
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
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("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("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[] = [
|
||||
{
|
||||
@@ -189,4 +646,30 @@ describe("PublicPageRenderer - 폼 필드 스타일", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user