Files
page-builder/tests/unit/ButtonPropertiesPanel.spec.tsx
T
jaybe a5b432fb7b
CI / test (push) Failing after 2m54s
CI / e2e (push) Has been skipped
CI / pr_and_merge (push) Has been skipped
TDD,E2E 개선, 미적용 스타일 개선
2025-12-07 09:52:42 +09:00

403 lines
12 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { ButtonPropertiesPanel } from "@/app/editor/panels/ButtonPropertiesPanel";
import type { ButtonBlockProps } from "@/features/editor/state/editorStore";
// ButtonPropertiesPanel 컨트롤 TDD
// - 색상 HEX 인풋 및 너비 모드 셀렉트가 updateBlock 을 올바르게 호출하는지 검증한다.
describe("ButtonPropertiesPanel", () => {
const baseProps: ButtonBlockProps = {
label: "버튼",
href: "#",
align: "left",
size: "md",
variant: "solid",
colorPalette: "primary",
};
afterEach(() => {
cleanup();
});
it("텍스트 색상 HEX 인풋 변경 시 updateBlock 이 textColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, textColorCustom: "#ffffff" }}
selectedBlockId="btn-1"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("버튼 텍스트 색상 HEX");
fireEvent.change(hexInput, { target: { value: "#112233" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-1",
expect.objectContaining({ textColorCustom: "#112233" }),
);
});
it("채움 색상 HEX 인풋 변경 시 updateBlock 이 fillColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, fillColorCustom: "#000000" }}
selectedBlockId="btn-2"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("버튼 채움 색상 HEX");
fireEvent.change(hexInput, { target: { value: "#445566" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-2",
expect.objectContaining({ fillColorCustom: "#445566" }),
);
});
it("외곽선 색상 HEX 인풋 변경 시 updateBlock 이 strokeColorCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, strokeColorCustom: "#000000" }}
selectedBlockId="btn-3"
updateBlock={updateBlock}
/>,
);
const hexInput = screen.getByLabelText("버튼 외곽선 색상 HEX");
fireEvent.change(hexInput, { target: { value: "#778899" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-3",
expect.objectContaining({ strokeColorCustom: "#778899" }),
);
});
it("버튼 너비 모드 셀렉트 변경 시 updateBlock 이 widthMode 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, widthMode: "auto", fullWidth: false }}
selectedBlockId="btn-4"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("버튼 너비 모드");
fireEvent.change(select, { target: { value: "fixed" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-4",
expect.objectContaining({ widthMode: "fixed" }),
);
});
it("버튼 이미지 URL 인풋 변경 시 updateBlock 이 imageSrc 와 imageSourceType(externalUrl) 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{
...baseProps,
imageSrc: "https://example.com/old.png",
imageSourceType: "externalUrl",
} as any}
selectedBlockId="btn-img-url"
updateBlock={updateBlock}
/>,
);
const urlInput = screen.getByLabelText("버튼 이미지 URL");
fireEvent.change(urlInput, { target: { value: "https://example.com/new.png" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-img-url",
expect.objectContaining({
imageSrc: "https://example.com/new.png",
imageSourceType: "externalUrl",
}),
);
});
it("버튼 이미지 위치 셀렉트 변경 시 updateBlock 이 imagePlacement 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{
...baseProps,
imagePlacement: "left",
imageSrc: "https://example.com/icon.png",
imageSourceType: "externalUrl",
} as any}
selectedBlockId="btn-img-pos"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("버튼 이미지 위치");
fireEvent.change(select, { target: { value: "right" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-img-pos",
expect.objectContaining({ imagePlacement: "right" }),
);
});
it("버튼 이미지 소스 셀렉트에서 URL/업로드를 전환할 수 있어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{
...baseProps,
imageSrc: "/api/image/test-id",
imageSourceType: "asset",
} as any}
selectedBlockId="btn-img-source"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("버튼 이미지 소스");
// 업로드 → URL 로 전환
fireEvent.change(select, { target: { value: "url" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-img-source",
expect.objectContaining({
imageSourceType: "externalUrl",
imageAssetId: null,
}),
);
});
it("버튼 텍스트 변경 시 updateBlock 이 label 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={baseProps}
selectedBlockId="btn-label"
updateBlock={updateBlock}
/>,
);
const textarea = screen.getByLabelText("버튼 텍스트");
fireEvent.change(textarea, { target: { value: "새 버튼" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-label",
expect.objectContaining({ label: "새 버튼" }),
);
});
it("가로 패딩 슬라이더 변경 시 updateBlock 이 paddingX 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, paddingX: 16 }}
selectedBlockId="btn-padding-x"
updateBlock={updateBlock}
/>,
);
const slider = screen.getByLabelText("가로 패딩 (px) 슬라이더");
fireEvent.change(slider, { target: { value: "24" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-padding-x",
expect.objectContaining({ paddingX: 24 }),
);
});
it("세로 패딩 슬라이더 변경 시 updateBlock 이 paddingY 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, paddingY: 10 }}
selectedBlockId="btn-padding-y"
updateBlock={updateBlock}
/>,
);
const slider = screen.getByLabelText("세로 패딩 (px) 슬라이더");
fireEvent.change(slider, { target: { value: "18" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-padding-y",
expect.objectContaining({ paddingY: 18 }),
);
});
it("버튼 링크 인풋 변경 시 updateBlock 이 href 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, href: "#" }}
selectedBlockId="btn-href"
updateBlock={updateBlock}
/>,
);
const input = screen.getByLabelText("버튼 링크");
fireEvent.change(input, { target: { value: "/signup" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-href",
expect.objectContaining({ href: "/signup" }),
);
});
it("버튼 정렬 셀렉트 변경 시 updateBlock 이 align 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, align: "left" }}
selectedBlockId="btn-align"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("버튼 정렬");
fireEvent.change(select, { target: { value: "center" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-align",
expect.objectContaining({ align: "center" }),
);
});
it("버튼 스타일 셀렉트 변경 시 updateBlock 이 variant 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, variant: "solid" }}
selectedBlockId="btn-variant"
updateBlock={updateBlock}
/>,
);
const select = screen.getByLabelText("버튼 스타일");
fireEvent.change(select, { target: { value: "outline" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-variant",
expect.objectContaining({ variant: "outline" }),
);
});
it("모서리 둥글기 슬라이더 변경 시 updateBlock 이 borderRadius 토큰으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, borderRadius: "md" }}
selectedBlockId="btn-radius"
updateBlock={updateBlock}
/>,
);
const slider = screen.getByLabelText("모서리 둥글기 슬라이더");
fireEvent.change(slider, { target: { value: "4" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-radius",
expect.objectContaining({ borderRadius: "full" }),
);
});
it("widthMode 가 fixed 일 때 고정 너비 슬라이더 변경 시 updateBlock 이 widthPx 로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, widthMode: "fixed", widthPx: 240 }}
selectedBlockId="btn-width-fixed"
updateBlock={updateBlock}
/>,
);
const slider = screen.getByLabelText("버튼 고정 너비 슬라이더");
fireEvent.change(slider, { target: { value: "300" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-width-fixed",
expect.objectContaining({ widthPx: 300 }),
);
});
it("버튼 크기 슬라이더 변경 시 updateBlock 이 fontSizeCustom 을 px 문자열로 호출해야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, fontSizeCustom: "16px" }}
selectedBlockId="btn-font-size"
updateBlock={updateBlock}
/>,
);
const slider = screen.getByLabelText("버튼 크기 슬라이더");
fireEvent.change(slider, { target: { value: "20" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-font-size",
expect.objectContaining({ fontSizeCustom: "20px" }),
);
});
it("줄 간격 슬라이더 변경 시 updateBlock 이 lineHeightCustom 으로 호출되어야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, lineHeightCustom: "1.4" }}
selectedBlockId="btn-line-height"
updateBlock={updateBlock}
/>,
);
const slider = screen.getByLabelText("줄 간격 슬라이더");
fireEvent.change(slider, { target: { value: "1.8" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-line-height",
expect.objectContaining({ lineHeightCustom: "1.8" }),
);
});
it("글자 간격 슬라이더 변경 시 updateBlock 이 letterSpacingCustom 을 em 단위로 호출해야 한다", () => {
const updateBlock = vi.fn();
render(
<ButtonPropertiesPanel
buttonProps={{ ...baseProps, letterSpacingCustom: "0em" }}
selectedBlockId="btn-letter-spacing"
updateBlock={updateBlock}
/>,
);
const slider = screen.getByLabelText("글자 간격 슬라이더");
fireEvent.change(slider, { target: { value: "16" } });
expect(updateBlock).toHaveBeenCalledWith(
"btn-letter-spacing",
expect.objectContaining({ letterSpacingCustom: "1em" }),
);
});
});