Files
page-builder/tests/unit/formHelpers.spec.ts
T
jaybe 48e13d2a75
CI / pr_and_merge (push) Has been skipped
CI / test (push) Failing after 46m46s
CI / test (pull_request) Failing after 43m8s
CI / pr_and_merge (pull_request) Has been skipped
video 블록 및 리펙터링
2025-11-27 10:07:59 +09:00

562 lines
19 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
computeFormInputExportTokens,
computeFormSelectExportTokens,
computeFormCheckboxExportTokens,
computeFormRadioExportTokens,
computeFormInputEditorTokens,
computeFormSelectEditorTokens,
computeFormOptionGroupEditorTokens,
computeFormInputPublicTokens,
computeFormSelectPublicTokens,
computeFormCheckboxPublicTokens,
computeFormRadioPublicTokens,
computeFormControllerPublicTokens,
computeFormBlockExportTokens,
} from "@/features/editor/utils/formHelpers";
import type {
Block,
FormBlockProps,
FormInputBlockProps,
FormSelectBlockProps,
FormCheckboxBlockProps,
FormRadioBlockProps,
} from "@/features/editor/state/editorStore";
const baseInput: FormInputBlockProps = {
label: "이름",
formFieldName: "name",
};
const baseSelect: FormSelectBlockProps = {
label: "플랜",
formFieldName: "plan",
options: [],
};
const baseCheckbox: FormCheckboxBlockProps = {
groupLabel: "옵션",
formFieldName: "options",
options: [],
};
const baseRadio: FormRadioBlockProps = {
groupLabel: "선택",
formFieldName: "choice",
options: [],
};
describe("formHelpers.computeFormInputExportTokens", () => {
it("텍스트 색상/배경/테두리/패딩/너비/둥글기 스타일을 계산해야 한다", () => {
const tokens = computeFormInputExportTokens({
...baseInput,
textColorCustom: " #ff0000 ",
fillColorCustom: " #111827 ",
strokeColorCustom: " #4b5563 ",
paddingX: 8,
paddingY: 4,
widthMode: "fixed",
widthPx: 300,
borderRadius: "lg",
});
expect(tokens.labelStyleAttr).toBe(' style="color:#ff0000"');
expect(tokens.inputStyleAttr).toContain("color:#ff0000");
expect(tokens.inputStyleAttr).toContain("background-color:#111827");
expect(tokens.inputStyleAttr).toContain("border-color:#4b5563");
expect(tokens.inputStyleAttr).toContain("padding-left:8px");
expect(tokens.inputStyleAttr).toContain("padding-right:8px");
expect(tokens.inputStyleAttr).toContain("padding-top:4px");
expect(tokens.inputStyleAttr).toContain("padding-bottom:4px");
expect(tokens.inputStyleAttr).toContain("width:300px");
expect(tokens.inputStyleAttr).toContain("border-radius:6px");
});
it("스타일 값이 없으면 style 속성이 비어 있어야 한다", () => {
const tokens = computeFormInputExportTokens(baseInput);
expect(tokens.labelStyleAttr).toBe("");
expect(tokens.inputStyleAttr).toBe(' style="border-radius:4px"');
});
});
describe("formHelpers.computeFormSelectExportTokens", () => {
it("텍스트 색상/배경/테두리/패딩/너비/둥글기 스타일을 계산해야 한다", () => {
const tokens = computeFormSelectExportTokens({
...baseSelect,
textColorCustom: " #00ff00 ",
fillColorCustom: " #020617 ",
strokeColorCustom: " #64748b ",
paddingX: 12,
paddingY: 6,
widthMode: "fixed",
widthPx: 240,
borderRadius: "full",
});
expect(tokens.labelStyleAttr).toBe(' style="color:#00ff00"');
expect(tokens.selectStyleAttr).toContain("color:#00ff00");
expect(tokens.selectStyleAttr).toContain("background-color:#020617");
expect(tokens.selectStyleAttr).toContain("border-color:#64748b");
expect(tokens.selectStyleAttr).toContain("padding-left:12px");
expect(tokens.selectStyleAttr).toContain("padding-right:12px");
expect(tokens.selectStyleAttr).toContain("padding-top:6px");
expect(tokens.selectStyleAttr).toContain("padding-bottom:6px");
expect(tokens.selectStyleAttr).toContain("width:240px");
expect(tokens.selectStyleAttr).toContain("border-radius:9999px");
});
it("스타일 값이 없으면 style 속성이 비어 있어야 한다", () => {
const tokens = computeFormSelectExportTokens(baseSelect);
expect(tokens.labelStyleAttr).toBe("");
expect(tokens.selectStyleAttr).toBe(' style="border-radius:4px"');
});
});
describe("formHelpers.computeFormCheckboxExportTokens", () => {
it("텍스트 색상/배경/테두리/패딩/둥글기 스타일을 계산해야 한다", () => {
const tokens = computeFormCheckboxExportTokens({
...baseCheckbox,
textColorCustom: " #38bdf8 ",
fillColorCustom: " #0f172a ",
strokeColorCustom: " #e5e7eb ",
paddingX: 10,
paddingY: 5,
borderRadius: "sm",
});
expect(tokens.labelStyleAttr).toBe(' style="color:#38bdf8"');
expect(tokens.optionStyleAttr).toContain("color:#38bdf8");
expect(tokens.optionStyleAttr).toContain("background-color:#0f172a");
expect(tokens.optionStyleAttr).toContain("border-color:#e5e7eb");
expect(tokens.optionStyleAttr).toContain("border-width:1px");
expect(tokens.optionStyleAttr).toContain("border-style:solid");
expect(tokens.optionStyleAttr).toContain("padding-left:10px");
expect(tokens.optionStyleAttr).toContain("padding-right:10px");
expect(tokens.optionStyleAttr).toContain("padding-top:5px");
expect(tokens.optionStyleAttr).toContain("padding-bottom:5px");
expect(tokens.optionStyleAttr).toContain("border-radius:2px");
});
it("스타일 값이 없으면 style 속성이 비어 있어야 한다", () => {
const tokens = computeFormCheckboxExportTokens(baseCheckbox);
expect(tokens.labelStyleAttr).toBe("");
expect(tokens.optionStyleAttr).toBe(' style="border-radius:4px"');
});
});
describe("formHelpers.computeFormRadioExportTokens", () => {
it("체크박스와 동일한 규칙으로 옵션 스타일을 계산해야 한다", () => {
const tokens = computeFormRadioExportTokens({
...baseRadio,
textColorCustom: " #fbbf24 ",
fillColorCustom: " #111827 ",
strokeColorCustom: " #facc15 ",
paddingX: 4,
paddingY: 2,
borderRadius: "md",
});
expect(tokens.labelStyleAttr).toBe(' style="color:#fbbf24"');
expect(tokens.optionStyleAttr).toContain("color:#fbbf24");
expect(tokens.optionStyleAttr).toContain("background-color:#111827");
expect(tokens.optionStyleAttr).toContain("border-color:#facc15");
expect(tokens.optionStyleAttr).toContain("border-width:1px");
expect(tokens.optionStyleAttr).toContain("border-style:solid");
expect(tokens.optionStyleAttr).toContain("padding-left:4px");
expect(tokens.optionStyleAttr).toContain("padding-right:4px");
expect(tokens.optionStyleAttr).toContain("padding-top:2px");
expect(tokens.optionStyleAttr).toContain("padding-bottom:2px");
expect(tokens.optionStyleAttr).toContain("border-radius:4px");
});
});
describe("formHelpers - editor tokens", () => {
it("computeFormInputEditorTokens: 에디터 인풋 필드 스타일을 계산해야 한다", () => {
const tokens = computeFormInputEditorTokens({
...baseInput,
textColorCustom: "#ff0000",
fillColorCustom: "#123456",
strokeColorCustom: "#654321",
widthMode: "fixed",
widthPx: 240,
borderRadius: "full",
align: "center",
labelLayout: "stacked",
} as any);
expect(tokens.fieldStyle.color).toBe("#ff0000");
expect(tokens.fieldStyle.backgroundColor).toBe("#123456");
expect(tokens.fieldStyle.borderColor).toBe("#654321");
expect(tokens.fieldStyle.width).toBe("240px");
expect(tokens.fieldStyle.borderRadius).toBe(9999);
});
it("computeFormSelectEditorTokens: 에디터 셀렉트 필드 스타일을 계산해야 한다", () => {
const tokens = computeFormSelectEditorTokens({
...baseSelect,
textColorCustom: "#ff0000",
fillColorCustom: "#123456",
strokeColorCustom: "#654321",
widthMode: "fixed",
widthPx: 260,
borderRadius: "lg",
} as any);
expect(tokens.fieldStyle.color).toBe("#ff0000");
expect(tokens.fieldStyle.backgroundColor).toBe("#123456");
expect(tokens.fieldStyle.borderColor).toBe("#654321");
expect(tokens.fieldStyle.width).toBe("260px");
expect(tokens.fieldStyle.borderRadius).toBe(9999);
});
it("computeFormOptionGroupEditorTokens: 에디터 라디오/체크박스 그룹 필드 스타일을 계산해야 한다", () => {
const tokens = computeFormOptionGroupEditorTokens({
...baseRadio,
textColorCustom: "#ff0000",
fillColorCustom: "#123456",
strokeColorCustom: "#654321",
widthMode: "fixed",
widthPx: 280,
borderRadius: "lg",
} as any);
expect(tokens.fieldStyle.color).toBe("#ff0000");
expect(tokens.fieldStyle.backgroundColor).toBe("#123456");
expect(tokens.fieldStyle.borderColor).toBe("#654321");
expect(tokens.fieldStyle.width).toBe("280px");
expect(tokens.fieldStyle.borderRadius).toBe(9999);
});
});
describe("formHelpers - public tokens", () => {
it("computeFormInputPublicTokens: 퍼블릭 인풋 필드 스타일을 계산해야 한다", () => {
const tokens = computeFormInputPublicTokens({
...baseInput,
align: "center",
widthMode: "fixed",
widthPx: 320,
paddingX: 16,
paddingY: 8,
textColorCustom: "#112233",
fillColorCustom: "#123456",
strokeColorCustom: "#ff0000",
borderRadius: "lg",
} as any);
expect(tokens.wrapperLayoutClass).toBe("flex flex-col gap-1");
expect(tokens.widthClass).toBe("");
expect(tokens.inputStyle.textAlign).toBe("center");
expect(tokens.inputStyle.width).toBe("20em");
expect(tokens.inputStyle.paddingInline).toBe("1em");
expect(tokens.inputStyle.paddingBlock).toBe("0.5em");
expect(tokens.inputStyle.color).toBe("#112233");
expect(tokens.inputStyle.backgroundColor).toBe("#123456");
expect(tokens.inputStyle.borderColor).toBe("#ff0000");
expect(tokens.inputStyle.borderRadius).toBe("6px");
});
it("computeFormSelectPublicTokens: 퍼블릭 셀렉트 필드 스타일을 계산해야 한다", () => {
const tokens = computeFormSelectPublicTokens({
...baseSelect,
widthMode: "fixed",
widthPx: 240,
paddingX: 16,
paddingY: 8,
textColorCustom: "#00ff00",
fillColorCustom: "#123456",
strokeColorCustom: "#ff0000",
borderRadius: "lg",
} as any);
expect(tokens.widthClass).toBe("");
expect(tokens.wrapperStyle.width).toBe("15em");
expect(tokens.selectStyle.paddingInline).toBe("1em");
expect(tokens.selectStyle.paddingBlock).toBe("0.5em");
expect(tokens.selectStyle.color).toBe("#00ff00");
expect(tokens.selectStyle.backgroundColor).toBe("#123456");
expect(tokens.selectStyle.borderColor).toBe("#ff0000");
expect(tokens.selectStyle.borderRadius).toBe("6px");
});
it("computeFormCheckboxPublicTokens: 퍼블릭 체크박스 그룹 스타일을 계산해야 한다", () => {
const tokens = computeFormCheckboxPublicTokens({
...baseCheckbox,
widthMode: "fixed",
widthPx: 320,
paddingX: 8,
paddingY: 4,
optionGapPx: 16,
textColorCustom: "#ffffff",
fillColorCustom: "#123456",
strokeColorCustom: "#ff0000",
borderRadius: "lg",
} as any);
expect(tokens.widthClass).toBe("");
expect(tokens.groupStyle.width).toBe("20em");
expect(tokens.optionsStyle.rowGap).toBe("1em");
expect(tokens.optionContainerStyle.paddingInline).toBe("0.5em");
expect(tokens.optionContainerStyle.paddingBlock).toBe("0.25em");
expect(tokens.optionContainerStyle.backgroundColor).toBe("#123456");
expect(tokens.optionContainerStyle.borderColor).toBe("#ff0000");
expect(tokens.optionContainerStyle.borderWidth).toBe("1px");
expect(tokens.optionContainerStyle.borderStyle).toBe("solid");
expect(tokens.optionContainerStyle.borderRadius).toBe("6px");
expect(tokens.groupTextStyle.color).toBe("#ffffff");
expect(tokens.optionTextStyle.color).toBe("#ffffff");
});
it("computeFormRadioPublicTokens: 퍼블릭 라디오 그룹 스타일을 계산해야 한다", () => {
const tokens = computeFormRadioPublicTokens({
...baseRadio,
widthMode: "fixed",
widthPx: 320,
paddingX: 8,
paddingY: 4,
optionGapPx: 16,
textColorCustom: "#ffffff",
fillColorCustom: "#123456",
strokeColorCustom: "#ff0000",
borderRadius: "lg",
} as any);
expect(tokens.widthClass).toBe("");
expect(tokens.groupStyle.width).toBe("20em");
expect(tokens.optionsStyle.rowGap).toBe("1em");
expect(tokens.optionContainerStyle.paddingInline).toBe("0.5em");
expect(tokens.optionContainerStyle.paddingBlock).toBe("0.25em");
expect(tokens.optionContainerStyle.backgroundColor).toBe("#123456");
expect(tokens.optionContainerStyle.borderColor).toBe("#ff0000");
expect(tokens.optionContainerStyle.borderWidth).toBe("1px");
expect(tokens.optionContainerStyle.borderStyle).toBe("solid");
expect(tokens.optionContainerStyle.borderRadius).toBe("6px");
expect(tokens.groupTextStyle.color).toBe("#ffffff");
expect(tokens.optionTextStyle.color).toBe("#ffffff");
});
});
describe("formHelpers.computeFormControllerPublicTokens", () => {
it("fieldIds 를 기반으로 컨트롤러 필드를 매핑하고 레이아웃/submitLabel 을 계산해야 한다", () => {
const formBlock: Block = {
id: "form-1",
type: "form",
props: {
kind: "contact",
submitTarget: "internal",
fieldIds: ["input-1", "checkbox-1"],
submitButtonId: "btn-1",
formWidthMode: "fixed",
formWidthPx: 320,
marginYPx: 32,
backgroundColorCustom: " #123456 ",
} as FormBlockProps,
} as Block;
const inputBlock: Block = {
id: "input-1",
type: "formInput",
props: {
label: "이름",
formFieldName: "name",
inputType: "email",
required: true,
} as any,
} as Block;
const checkboxBlock: Block = {
id: "checkbox-1",
type: "formCheckbox",
props: {
groupLabel: "옵션",
formFieldName: "options",
required: true,
groupLabelMode: "image",
groupLabelImageUrl: "https://example.com/group.png",
options: [
{ label: "A", value: "a" },
{ label: "B", value: "b" },
],
} as any,
} as Block;
const buttonBlock: Block = {
id: "btn-1",
type: "button",
props: {
label: "컨트롤러 버튼",
} as any,
} as Block;
const blocks: Block[] = [formBlock, inputBlock, checkboxBlock, buttonBlock];
const tokens = computeFormControllerPublicTokens(formBlock, blocks);
expect(tokens.fields).toHaveLength(2);
const inputField = tokens.fields[0];
const checkboxField = tokens.fields[1];
expect(inputField.id).toBe("input-1");
expect(inputField.name).toBe("name");
expect(inputField.label).toBe("이름");
expect(inputField.type).toBe("text");
expect(inputField.required).toBe(true);
expect(checkboxField.id).toBe("checkbox-1");
expect(checkboxField.name).toBe("options");
expect(checkboxField.label).toBe("옵션");
expect(checkboxField.type).toBe("checkbox");
expect(checkboxField.required).toBe(true);
expect(checkboxField.options).toHaveLength(2);
expect(checkboxField.groupLabelMode).toBe("image");
expect(checkboxField.groupLabelImageUrl).toBe("https://example.com/group.png");
expect(tokens.formClassName).toBe("space-y-3");
expect(tokens.formStyle.width).toBe("20em");
expect(tokens.formStyle.marginTop).toBe("2em");
expect(tokens.formStyle.marginBottom).toBe("2em");
expect(tokens.formStyle.backgroundColor).toBe("#123456");
expect(tokens.submitLabel).toBe("컨트롤러 버튼");
});
it("fields 가 있을 때는 fields 를 사용하고, 없으면 빈 배열을 반환해야 한다", () => {
const formWithFields: Block = {
id: "form-2",
type: "form",
props: {
kind: "contact",
submitTarget: "internal",
fields: [
{
id: "f1",
name: "email",
label: "이메일",
type: "email",
required: true,
},
],
} as FormBlockProps,
} as Block;
const tokensWithFields = computeFormControllerPublicTokens(formWithFields, [formWithFields]);
expect(tokensWithFields.fields).toHaveLength(1);
expect(tokensWithFields.fields[0].id).toBe("f1");
expect(tokensWithFields.fields[0].name).toBe("email");
expect(tokensWithFields.fields[0].label).toBe("이메일");
expect(tokensWithFields.fields[0].type).toBe("email");
expect(tokensWithFields.fields[0].required).toBe(true);
const formFallback: Block = {
id: "form-3",
type: "form",
props: {
kind: "contact",
submitTarget: "internal",
} as FormBlockProps,
} as Block;
const tokensFallback = computeFormControllerPublicTokens(formFallback, [formFallback]);
expect(tokensFallback.fields).toHaveLength(0);
});
});
describe("formHelpers.computeFormBlockExportTokens", () => {
it("fieldIds 가 있을 때 컨트롤러 필드와 폼 배경 스타일을 계산해야 한다", () => {
const formBlock: Block = {
id: "form-export-1",
type: "form",
props: {
kind: "contact",
submitTarget: "internal",
fieldIds: ["input-1", "select-1"],
fields: [],
backgroundColorCustom: " #123456 ",
} as FormBlockProps,
} as Block;
const inputBlock: Block = {
id: "input-1",
type: "formInput",
props: {
label: "이름",
formFieldName: "name",
inputType: "email",
required: true,
} as any,
} as Block;
const selectBlock: Block = {
id: "select-1",
type: "formSelect",
props: {
label: "플랜",
formFieldName: "plan",
options: [
{ label: "Basic", value: "basic" },
{ label: "Pro", value: "pro" },
],
} as any,
} as Block;
const blocks: Block[] = [formBlock, inputBlock, selectBlock];
const tokens = computeFormBlockExportTokens(formBlock, blocks);
expect(tokens.hasControllerFields).toBe(true);
expect(tokens.controllerFields).toHaveLength(2);
expect(tokens.controllerFields[0].id).toBe("input-1");
expect(tokens.controllerFields[1].id).toBe("select-1");
expect(tokens.fallbackFields).toHaveLength(0);
expect(tokens.formStyleParts).toEqual(["background-color:#123456"]);
});
it("fieldIds 가 없고 fields 가 있을 때 fallbackFields 를 사용해야 한다", () => {
const formBlock: Block = {
id: "form-export-2",
type: "form",
props: {
kind: "contact",
submitTarget: "internal",
fieldIds: [],
fields: [
{
id: "f1",
name: "email",
label: "이메일",
type: "email",
required: true,
},
],
backgroundColorCustom: undefined,
} as FormBlockProps,
} as Block;
const tokens = computeFormBlockExportTokens(formBlock, [formBlock]);
expect(tokens.hasControllerFields).toBe(false);
expect(tokens.controllerFields).toHaveLength(0);
expect(tokens.fallbackFields).toHaveLength(1);
expect(tokens.fallbackFields[0].id).toBe("f1");
expect(tokens.formStyleParts).toEqual([]);
});
it("fieldIds 와 fields 가 모두 없으면 fallbackFields 도 빈 배열이어야 한다", () => {
const formBlock: Block = {
id: "form-export-3",
type: "form",
props: {
kind: "contact",
submitTarget: "internal",
} as FormBlockProps,
} as Block;
const tokens = computeFormBlockExportTokens(formBlock, [formBlock]);
expect(tokens.hasControllerFields).toBe(false);
expect(tokens.controllerFields).toHaveLength(0);
expect(tokens.fallbackFields).toHaveLength(0);
});
});