테마 적용
This commit is contained in:
@@ -171,6 +171,56 @@ describe("AllProjectSubmissionsPage - 전체 폼 제출 내역", () => {
|
||||
expect(submissionsLink.closest("a")?.getAttribute("href")).toBe("/projects/submissions");
|
||||
});
|
||||
|
||||
it("전체 제출 내역 테이블은 라이트 테마에서 읽기 쉬운 텍스트 색상을 사용해야 한다", async () => {
|
||||
const submissions = [
|
||||
{
|
||||
id: "1",
|
||||
createdAt: "2025-01-01T12:00:00.000Z",
|
||||
projectSlug: "proj-1",
|
||||
projectTitle: "프로젝트 1",
|
||||
payload: {
|
||||
name: "홍길동",
|
||||
email: "user@example.com",
|
||||
phone: "010-1234-5678",
|
||||
birthdate: "1990-01-01",
|
||||
message: "안녕하세요",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(submissions), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
const { container } = render(<AllProjectSubmissionsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const headerRow = container.querySelector("thead tr") as HTMLElement | null;
|
||||
expect(headerRow).not.toBeNull();
|
||||
|
||||
const headerClass = headerRow!.className;
|
||||
expect(headerClass).toContain("border-slate-200");
|
||||
expect(headerClass).toContain("text-slate-600");
|
||||
expect(headerClass).toContain("dark:border-slate-800");
|
||||
expect(headerClass).toContain("dark:text-slate-400");
|
||||
|
||||
const nameCellNode = await screen.findByText("홍길동");
|
||||
const nameCell = nameCellNode.closest("td") as HTMLElement | null;
|
||||
expect(nameCell).not.toBeNull();
|
||||
|
||||
const nameClass = nameCell!.className;
|
||||
expect(nameClass).toContain("text-slate-900");
|
||||
expect(nameClass).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("GNB에서 현재 페이지인 '전체 제출 내역' 탭에 aria-current=\"page\"가 설정되어야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify([]), {
|
||||
|
||||
@@ -210,5 +210,77 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
// 다시 클릭하면 펼쳐져야 한다.
|
||||
fireEvent.click(templateToggle);
|
||||
expect(screen.getByRole("button", { name: "Hero 템플릿" })).toBeDefined();
|
||||
});
|
||||
|
||||
it("블록 추가 버튼들은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
const textButton = screen.getByRole("button", { name: "텍스트" });
|
||||
const className = textButton.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 밝은 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-slate-50");
|
||||
expect(className).toContain("text-slate-900");
|
||||
|
||||
// 다크 모드: 기존 다크 스타일 유지
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("히어로 템플릿 카드는 라이트/다크 테마에 맞는 카드 배경/테두리 클래스를 사용해야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
const heroDescription = screen.getByText(
|
||||
"페이지 상단 Hero 섹션 (큰 제목 + 서브텍스트 + 버튼)",
|
||||
);
|
||||
const card = heroDescription.closest("div");
|
||||
expect(card).not.toBeNull();
|
||||
|
||||
const className = card!.getAttribute("class") ?? "";
|
||||
expect(className).toContain("bg-slate-50");
|
||||
expect(className).toContain("border-slate-200");
|
||||
expect(className).toContain("dark:border-slate-800");
|
||||
expect(className).toContain("dark:bg-slate-950/50");
|
||||
});
|
||||
|
||||
it("히어로 템플릿 추가 버튼은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
const heroButton = screen.getByRole("button", { name: "Hero 템플릿" });
|
||||
const className = heroButton.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 밝은 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-slate-50");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 기존 다크 톤 유지
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("히어로 템플릿 썸네일 프레임은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
const thumb = screen.getByTestId("template-preview-hero") as HTMLDivElement;
|
||||
const className = thumb.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 밝은 카드 배경 + 연한 테두리
|
||||
expect(className).toContain("bg-slate-100");
|
||||
expect(className).toContain("border-slate-300");
|
||||
// 다크 모드: 기존 다크 카드 톤 유지
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:border-slate-700");
|
||||
});
|
||||
|
||||
it("사이드바 루트는 라이트/다크 테마에 맞는 배경/테두리 클래스를 사용해야 한다", () => {
|
||||
const { container } = render(<BlocksSidebar />);
|
||||
|
||||
const aside = container.querySelector("aside") as HTMLElement | null;
|
||||
expect(aside).not.toBeNull();
|
||||
|
||||
const className = aside!.getAttribute("class") ?? "";
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("border-slate-200");
|
||||
expect(className).toContain("dark:border-slate-800");
|
||||
expect(className).toContain("dark:bg-slate-950/40");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,6 +80,46 @@ describe("ButtonPropertiesPanel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("텍스트 색상 팔레트에서 \"없음\" 을 선택하면 textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ButtonPropertiesPanel
|
||||
buttonProps={{ ...baseProps, textColorCustom: "#ff0000" }}
|
||||
selectedBlockId="btn-text-none"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 첫 번째 ColorPickerField(텍스트 색상)의 팔레트 버튼을 연다.
|
||||
const paletteButtons = screen.getAllByText("색상 팔레트");
|
||||
fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement);
|
||||
|
||||
// 팔레트에서 "없음" 항목을 버튼 role 기준으로 선택한다.
|
||||
const noneButtons = screen.getAllByRole("button", { name: "없음" });
|
||||
fireEvent.click(noneButtons[0]);
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
"btn-text-none",
|
||||
expect.objectContaining({ textColorCustom: "" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("textColorCustom 이 비어 있으면 버튼 텍스트 색상 HEX 인풋 값은 빈 문자열이어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ButtonPropertiesPanel
|
||||
buttonProps={{ ...baseProps, textColorCustom: "" }}
|
||||
selectedBlockId="btn-text-empty"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("버튼 텍스트 색상 HEX") as HTMLInputElement;
|
||||
expect(hexInput.value).toBe("");
|
||||
});
|
||||
|
||||
it("버튼 너비 모드 셀렉트 변경 시 updateBlock 이 widthMode 로 호출되어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
@@ -200,6 +240,28 @@ describe("ButtonPropertiesPanel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("버튼 텍스트 textarea 는 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ButtonPropertiesPanel
|
||||
buttonProps={baseProps}
|
||||
selectedBlockId="btn-theme"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const textarea = screen.getByLabelText("버튼 텍스트") as HTMLTextAreaElement;
|
||||
const className = textarea.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 다크 배경 + 밝은 텍스트
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("가로 패딩 슬라이더 변경 시 updateBlock 이 paddingX 로 호출되어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
@@ -399,4 +461,43 @@ describe("ButtonPropertiesPanel", () => {
|
||||
expect.objectContaining({ letterSpacingCustom: "1em" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("버튼 주요 인풋/셀렉트 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ButtonPropertiesPanel
|
||||
buttonProps={baseProps as any}
|
||||
selectedBlockId="btn-theme"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const labelTextarea = screen.getByLabelText("버튼 텍스트") as HTMLTextAreaElement;
|
||||
const labelClass = labelTextarea.getAttribute("class") ?? "";
|
||||
expect(labelClass).toContain("bg-white");
|
||||
expect(labelClass).toContain("text-slate-900");
|
||||
expect(labelClass).toContain("border-slate-300");
|
||||
expect(labelClass).toContain("dark:bg-slate-900");
|
||||
expect(labelClass).toContain("dark:text-slate-100");
|
||||
expect(labelClass).toContain("dark:border-slate-700");
|
||||
|
||||
const imageSourceSelect = screen.getByLabelText("버튼 이미지 소스") as HTMLSelectElement;
|
||||
const imageSourceClass = imageSourceSelect.getAttribute("class") ?? "";
|
||||
expect(imageSourceClass).toContain("bg-white");
|
||||
expect(imageSourceClass).toContain("text-slate-900");
|
||||
expect(imageSourceClass).toContain("border-slate-300");
|
||||
expect(imageSourceClass).toContain("dark:bg-slate-900");
|
||||
expect(imageSourceClass).toContain("dark:text-slate-100");
|
||||
expect(imageSourceClass).toContain("dark:border-slate-700");
|
||||
|
||||
const alignSelect = screen.getByLabelText("버튼 정렬") as HTMLSelectElement;
|
||||
const alignClass = alignSelect.getAttribute("class") ?? "";
|
||||
expect(alignClass).toContain("bg-white");
|
||||
expect(alignClass).toContain("text-slate-900");
|
||||
expect(alignClass).toContain("border-slate-300");
|
||||
expect(alignClass).toContain("dark:bg-slate-900");
|
||||
expect(alignClass).toContain("dark:text-slate-100");
|
||||
expect(alignClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { ColorPickerField, TEXT_COLOR_PALETTE } from "@/features/editor/components/ColorPickerField";
|
||||
|
||||
describe("ColorPickerField - 테마", () => {
|
||||
it("HEX 텍스트 인풋은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(
|
||||
<ColorPickerField
|
||||
label="색상"
|
||||
ariaLabelColorInput="컬러 입력"
|
||||
ariaLabelHexInput="HEX 입력"
|
||||
value="#ff0000"
|
||||
onChange={() => {}}
|
||||
palette={TEXT_COLOR_PALETTE}
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("HEX 입력") as HTMLInputElement;
|
||||
const className = hexInput.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 다크 배경 + 밝은 텍스트
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("팔레트 드롭다운 요약 버튼은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(
|
||||
<ColorPickerField
|
||||
label="색상"
|
||||
ariaLabelColorInput="컬러 입력"
|
||||
ariaLabelHexInput="HEX 입력"
|
||||
value="#ff0000"
|
||||
onChange={() => {}}
|
||||
palette={TEXT_COLOR_PALETTE}
|
||||
/>,
|
||||
);
|
||||
|
||||
const [label] = screen.getAllByText("색상 팔레트");
|
||||
const button = label.closest("button") as HTMLButtonElement | null;
|
||||
expect(button).not.toBeNull();
|
||||
|
||||
const className = button!.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 밝은 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 다크 배경 + 밝은 텍스트
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("팔레트 드롭다운 항목은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
render(
|
||||
<ColorPickerField
|
||||
label="색상"
|
||||
ariaLabelColorInput="컬러 입력"
|
||||
ariaLabelHexInput="HEX 입력"
|
||||
value="#ff0000"
|
||||
onChange={() => {}}
|
||||
palette={TEXT_COLOR_PALETTE}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 드롭다운을 연다
|
||||
const [label] = screen.getAllByText("색상 팔레트");
|
||||
const toggleButton = label.closest("button") as HTMLButtonElement | null;
|
||||
expect(toggleButton).not.toBeNull();
|
||||
fireEvent.click(toggleButton!);
|
||||
|
||||
// 팔레트 첫 항목("기본") 버튼을 찾는다
|
||||
const itemButton = screen.getByRole("button", { name: "기본" }) as HTMLButtonElement;
|
||||
const className = itemButton.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 다크 배경 + 밝은 텍스트
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
|
||||
// 색상 불렛(span)은 테두리를 가져야 한다 (흰색/투명 색상도 배경과 구분되도록)
|
||||
const bullet = itemButton.querySelector("span span") as HTMLSpanElement | null;
|
||||
expect(bullet).not.toBeNull();
|
||||
const bulletClass = bullet!.getAttribute("class") ?? "";
|
||||
expect(bulletClass).toContain("rounded-full");
|
||||
expect(bulletClass).toContain("border");
|
||||
expect(bulletClass).toContain("border-slate-300");
|
||||
expect(bulletClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
});
|
||||
@@ -143,4 +143,43 @@ describe("DividerPropertiesPanel", () => {
|
||||
expect.objectContaining({ marginYPx: 24 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("DividerPropertiesPanel 주요 셀렉트 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<DividerPropertiesPanel
|
||||
dividerProps={baseProps}
|
||||
selectedBlockId="divider-theme-1"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const alignSelect = screen.getByLabelText("구분선 정렬") as HTMLSelectElement;
|
||||
const alignClass = alignSelect.getAttribute("class") ?? "";
|
||||
expect(alignClass).toContain("bg-white");
|
||||
expect(alignClass).toContain("text-slate-900");
|
||||
expect(alignClass).toContain("border-slate-300");
|
||||
expect(alignClass).toContain("dark:bg-slate-900");
|
||||
expect(alignClass).toContain("dark:text-slate-100");
|
||||
expect(alignClass).toContain("dark:border-slate-700");
|
||||
|
||||
const thicknessSelect = screen.getByLabelText("구분선 두께") as HTMLSelectElement;
|
||||
const thicknessClass = thicknessSelect.getAttribute("class") ?? "";
|
||||
expect(thicknessClass).toContain("bg-white");
|
||||
expect(thicknessClass).toContain("text-slate-900");
|
||||
expect(thicknessClass).toContain("border-slate-300");
|
||||
expect(thicknessClass).toContain("dark:bg-slate-900");
|
||||
expect(thicknessClass).toContain("dark:text-slate-100");
|
||||
expect(thicknessClass).toContain("dark:border-slate-700");
|
||||
|
||||
const widthModeSelect = screen.getByLabelText("구분선 길이 모드") 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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import { render, screen, cleanup } from "@testing-library/react";
|
||||
import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
|
||||
import { EditorCanvas } from "@/app/editor/EditorCanvas";
|
||||
import { EditorCanvas, EditorCanvasDragPreview } from "@/app/editor/EditorCanvas";
|
||||
|
||||
// 에디터 캔버스/페이지 배경색 적용 규칙에 대한 최소 TDD
|
||||
// - canvasBgColorHex: 실제 캔버스 영역(editor-canvas-inner)의 배경색
|
||||
@@ -73,4 +73,30 @@ describe("EditorCanvas - 캔버스/페이지 배경색", () => {
|
||||
// custom 프리셋에서는 canvasWidthPx 값이 그대로 사용된다.
|
||||
expect(renderWithConfig({ canvasPreset: "custom", canvasWidthPx: 1024 })).toBe("1024px");
|
||||
});
|
||||
|
||||
it("DragOverlay 프리뷰는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const block: Block = {
|
||||
id: "drag_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "드래그 미리보기",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const { container } = render(<EditorCanvasDragPreview block={block} />);
|
||||
|
||||
const overlay = container.querySelector(
|
||||
"div.pointer-events-none.rounded.border",
|
||||
) as HTMLElement | null;
|
||||
expect(overlay).not.toBeNull();
|
||||
|
||||
const className = overlay!.getAttribute("class") ?? "";
|
||||
expect(className).toContain("border-sky-500");
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,6 +214,28 @@ describe("EditorPage - 폼 블록 스타일", () => {
|
||||
expect(input.className).toContain("pb-input");
|
||||
});
|
||||
|
||||
it("formInput 에디터 렌더에는 text-slate-* 색상 클래스가 없어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_input_neutral_editor",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "폼 입력",
|
||||
formFieldName: "input_neutral",
|
||||
inputType: "text",
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
mockState.blocks = blocks;
|
||||
mockState.selectedBlockId = "form_input_neutral_editor";
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const label = screen.getByText("폼 입력");
|
||||
expect(label.className).not.toContain("text-slate-");
|
||||
});
|
||||
|
||||
it("formSelect: textColorCustom / fillColorCustom / strokeColorCustom / widthPx / borderRadius 가 에디터 렌더에 반영되어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
@@ -275,6 +297,30 @@ describe("EditorPage - 폼 블록 스타일", () => {
|
||||
expect(select.className).toContain("pb-select");
|
||||
});
|
||||
|
||||
it("formSelect 에디터 렌더에는 text-slate-* 색상 클래스가 없어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_select_neutral_editor",
|
||||
type: "formSelect",
|
||||
props: {
|
||||
label: "폼 셀렉트",
|
||||
formFieldName: "select_neutral",
|
||||
options: [
|
||||
{ label: "A", value: "a" },
|
||||
],
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
mockState.blocks = blocks;
|
||||
mockState.selectedBlockId = "form_select_neutral_editor";
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const label = screen.getByText("폼 셀렉트");
|
||||
expect(label.className).not.toContain("text-slate-");
|
||||
});
|
||||
|
||||
it("formRadio: textColorCustom / fillColorCustom / strokeColorCustom / widthPx / borderRadius 가 에디터 렌더에 반영되어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
@@ -313,6 +359,87 @@ describe("EditorPage - 폼 블록 스타일", () => {
|
||||
expect(groupContainer.style.borderRadius).toBe("6px");
|
||||
});
|
||||
|
||||
it("formRadio 에디터 그룹 컨테이너에는 text-slate-* 색상 클래스가 없어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_radio_neutral_editor",
|
||||
type: "formRadio",
|
||||
props: {
|
||||
formFieldName: "radio-neutral",
|
||||
groupLabel: "라디오 그룹",
|
||||
options: [
|
||||
{ label: "옵션 A", value: "a" },
|
||||
],
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
mockState.blocks = blocks;
|
||||
mockState.selectedBlockId = "form_radio_neutral_editor";
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const label = screen.getByText("라디오 그룹");
|
||||
const groupContainer = label.closest("div") as HTMLElement;
|
||||
expect(groupContainer.className).not.toContain("text-slate-");
|
||||
});
|
||||
|
||||
it("formRadio 에디터 프리뷰의 옵션 input 은 OS 기본 스타일을 사용하기 위해 색상 관련 Tailwind 클래스(bg-/text-/border-)를 사용하지 않아야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_radio_os_editor",
|
||||
type: "formRadio",
|
||||
props: {
|
||||
formFieldName: "radio-os",
|
||||
groupLabel: "라디오 OS",
|
||||
options: [{ label: "옵션 A", value: "a" }],
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
mockState.blocks = blocks;
|
||||
mockState.selectedBlockId = "form_radio_os_editor";
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const label = screen.getByText("라디오 OS");
|
||||
const groupContainer = label.closest("div") as HTMLElement;
|
||||
const input = groupContainer.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 에디터 프리뷰의 옵션 input 은 OS 기본 스타일을 사용하기 위해 색상 관련 Tailwind 클래스(bg-/text-/border-)를 사용하지 않아야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_checkbox_os_editor",
|
||||
type: "formCheckbox",
|
||||
props: {
|
||||
formFieldName: "check-os",
|
||||
groupLabel: "체크 OS",
|
||||
options: [{ label: "체크 1", value: "c1" }],
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
mockState.blocks = blocks;
|
||||
mockState.selectedBlockId = "form_checkbox_os_editor";
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const label = screen.getByText("체크 OS");
|
||||
const groupContainer = label.closest("div") as HTMLElement;
|
||||
const input = groupContainer.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: textColorCustom / fillColorCustom / strokeColorCustom / widthPx / borderRadius 가 에디터 렌더에 반영되어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
@@ -351,6 +478,31 @@ describe("EditorPage - 폼 블록 스타일", () => {
|
||||
expect(groupContainer.style.borderRadius).toBe("6px");
|
||||
});
|
||||
|
||||
it("formCheckbox 에디터 그룹 컨테이너에는 text-slate-* 색상 클래스가 없어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_checkbox_neutral_editor",
|
||||
type: "formCheckbox",
|
||||
props: {
|
||||
formFieldName: "checkbox-neutral",
|
||||
groupLabel: "체크 그룹",
|
||||
options: [
|
||||
{ label: "체크 1", value: "c1" },
|
||||
],
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
mockState.blocks = blocks;
|
||||
mockState.selectedBlockId = "form_checkbox_neutral_editor";
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const label = screen.getByText("체크 그룹");
|
||||
const groupContainer = label.closest("div") as HTMLElement;
|
||||
expect(groupContainer.className).not.toContain("text-slate-");
|
||||
});
|
||||
|
||||
it("formRadio: 그룹 타이틀 레이아웃이 inline 이면 에디터에서도 라벨과 필드 컨테이너가 가로 정렬되고 columnGap 이 labelGapPx 로 반영되어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { render, screen, cleanup } from "@testing-library/react";
|
||||
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
||||
import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
|
||||
import EditorPage from "@/app/editor/page";
|
||||
|
||||
@@ -97,4 +97,100 @@ describe("EditorPage - 페이지/캔버스 배경", () => {
|
||||
const canvasOuter = screen.getByTestId("editor-canvas") as HTMLElement;
|
||||
expect(canvasOuter.style.backgroundColor).toBe("rgb(34, 34, 34)");
|
||||
});
|
||||
|
||||
it("main 요소는 전역 라이트/다크 테마 클래스(bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-50)를 사용해야 한다", () => {
|
||||
const { container } = render(<EditorPage />);
|
||||
|
||||
const mainEl = container.querySelector("main") as HTMLElement | null;
|
||||
expect(mainEl).not.toBeNull();
|
||||
|
||||
const className = mainEl!.getAttribute("class") ?? "";
|
||||
expect(className).toContain("bg-slate-100");
|
||||
expect(className).toContain("text-slate-900");
|
||||
expect(className).toContain("dark:bg-slate-950");
|
||||
expect(className).toContain("dark:text-slate-50");
|
||||
});
|
||||
|
||||
it("프로젝트 저장/불러오기 모달 카드는 라이트/다크 테마에 맞는 카드/인풋 크롬을 사용해야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByRole("button", { name: /메뉴/ });
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByRole("button", { name: "프로젝트 저장/불러오기" });
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const heading = screen.getByText("프로젝트 저장 / 불러오기");
|
||||
const headerDiv = heading.closest("div") as HTMLDivElement | null;
|
||||
expect(headerDiv).not.toBeNull();
|
||||
|
||||
const card = headerDiv!.parentElement as HTMLDivElement | null;
|
||||
expect(card).not.toBeNull();
|
||||
|
||||
const cardClass = card!.getAttribute("class") ?? "";
|
||||
expect(cardClass).toContain("bg-white");
|
||||
expect(cardClass).toContain("text-slate-900");
|
||||
expect(cardClass).toContain("border-slate-200");
|
||||
expect(cardClass).toContain("dark:bg-slate-900");
|
||||
expect(cardClass).toContain("dark:text-slate-100");
|
||||
expect(cardClass).toContain("dark:border-slate-700");
|
||||
|
||||
const titleSpan = Array.from(card!.querySelectorAll("span")).find(
|
||||
(el) => el.textContent === "프로젝트 제목",
|
||||
);
|
||||
expect(titleSpan).not.toBeUndefined();
|
||||
const titleWrapper = (titleSpan as HTMLSpanElement).parentElement as HTMLElement | null;
|
||||
expect(titleWrapper).not.toBeNull();
|
||||
|
||||
const titleInput = titleWrapper!.querySelector("input") as HTMLInputElement | null;
|
||||
expect(titleInput).not.toBeNull();
|
||||
|
||||
const inputClass = titleInput!.getAttribute("class") ?? "";
|
||||
expect(inputClass).toContain("bg-white");
|
||||
expect(inputClass).toContain("text-slate-900");
|
||||
expect(inputClass).toContain("border-slate-300");
|
||||
expect(inputClass).toContain("dark:bg-slate-900");
|
||||
expect(inputClass).toContain("dark:text-slate-100");
|
||||
expect(inputClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
|
||||
it("JSON Export / Import 모달 카드는 라이트/다크 테마에 맞는 카드/textarea 크롬을 사용해야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByRole("button", { name: /메뉴/ });
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const jsonMenuItem = screen.getByRole("button", { name: "JSON 내보내기/불러오기" });
|
||||
fireEvent.click(jsonMenuItem);
|
||||
|
||||
const heading = screen.getByText(/JSON Export \/ Import/);
|
||||
const headerDiv = heading.closest("div") as HTMLDivElement | null;
|
||||
expect(headerDiv).not.toBeNull();
|
||||
|
||||
const card = headerDiv!.parentElement as HTMLDivElement | null;
|
||||
expect(card).not.toBeNull();
|
||||
|
||||
const cardClass = card!.getAttribute("class") ?? "";
|
||||
expect(cardClass).toContain("bg-white");
|
||||
expect(cardClass).toContain("text-slate-900");
|
||||
expect(cardClass).toContain("border-slate-200");
|
||||
expect(cardClass).toContain("dark:bg-slate-900");
|
||||
expect(cardClass).toContain("dark:text-slate-100");
|
||||
expect(cardClass).toContain("dark:border-slate-700");
|
||||
|
||||
const jsonLabel = screen.getByText("에디터 상태 JSON");
|
||||
const jsonWrapper = jsonLabel.parentElement as HTMLElement | null;
|
||||
expect(jsonWrapper).not.toBeNull();
|
||||
|
||||
const textarea = jsonWrapper!.querySelector("textarea") as HTMLTextAreaElement | null;
|
||||
expect(textarea).not.toBeNull();
|
||||
|
||||
const textareaClass = textarea!.getAttribute("class") ?? "";
|
||||
expect(textareaClass).toContain("bg-white");
|
||||
expect(textareaClass).toContain("text-slate-900");
|
||||
expect(textareaClass).toContain("border-slate-300");
|
||||
expect(textareaClass).toContain("dark:bg-slate-900");
|
||||
expect(textareaClass).toContain("dark:text-slate-100");
|
||||
expect(textareaClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,6 +87,90 @@ vi.mock("next/navigation", () => {
|
||||
});
|
||||
|
||||
describe("EditorPage - 선택 포커스", () => {
|
||||
it("선택/비선택 블록 셸은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "첫 번째 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "blk_2",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "두 번째 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
mockState.blocks = blocks;
|
||||
mockState.selectedBlockId = "blk_2";
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const allBlocks = screen.getAllByTestId("editor-block") as HTMLElement[];
|
||||
const selected = allBlocks.find((el) => el.dataset.selected === "true");
|
||||
const nonSelected = allBlocks.find((el) => el.dataset.selected !== "true");
|
||||
|
||||
expect(selected).toBeTruthy();
|
||||
expect(nonSelected).toBeTruthy();
|
||||
|
||||
const selectedClass = selected!.className;
|
||||
const nonSelectedClass = nonSelected!.className;
|
||||
|
||||
// 선택된 블록: 하이라이트 보더 + 라이트/다크 배경/텍스트 듀얼 테마
|
||||
expect(selectedClass).toContain("border-sky-500");
|
||||
expect(selectedClass).toContain("bg-sky-50");
|
||||
expect(selectedClass).toContain("text-slate-900");
|
||||
expect(selectedClass).toContain("dark:bg-slate-900/80");
|
||||
expect(selectedClass).toContain("dark:text-slate-100");
|
||||
|
||||
// 비선택 블록: 기본 카드 크롬(라이트/다크 듀얼)
|
||||
expect(nonSelectedClass).toContain("border-slate-200");
|
||||
expect(nonSelectedClass).toContain("bg-white");
|
||||
expect(nonSelectedClass).toContain("text-slate-900");
|
||||
expect(nonSelectedClass).toContain("dark:border-slate-700");
|
||||
expect(nonSelectedClass).toContain("dark:bg-slate-900");
|
||||
expect(nonSelectedClass).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("블록 드래그 핸들은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "첫 번째 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
mockState.blocks = blocks;
|
||||
mockState.selectedBlockId = "blk_1";
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const handle = screen.getByLabelText("블록 드래그 핸들") as HTMLButtonElement;
|
||||
const className = handle.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 밝은 버튼 크롬
|
||||
expect(className).toContain("border-slate-300");
|
||||
expect(className).toContain("bg-slate-50");
|
||||
expect(className).toContain("text-slate-500");
|
||||
// 다크 모드: 기존 다크 톤 유지
|
||||
expect(className).toContain("dark:border-slate-700");
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-400");
|
||||
});
|
||||
|
||||
it("선택된 블록은 editor-block 에 border-sky-500 / bg-slate-900/80 하이라이트를 가져야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
@@ -150,6 +234,40 @@ describe("EditorPage - 선택 포커스", () => {
|
||||
expect(sectionEl.className).toContain("border-sky-500");
|
||||
});
|
||||
|
||||
it("EditorPage 레이아웃은 main 에 h-screen 컨테이너를 사용하고, 좌/중/우 컬럼이 각각 스크롤되어야 한다", () => {
|
||||
mockState.blocks = [];
|
||||
mockState.selectedBlockId = null;
|
||||
|
||||
const { container } = render(<EditorPage />);
|
||||
|
||||
const main = container.querySelector("main") as HTMLElement;
|
||||
expect(main).toBeTruthy();
|
||||
const mainClass = main.getAttribute("class") ?? "";
|
||||
// 전체 에디터는 뷰포트 높이에 맞춘 h-screen 컨테이너 안에서 동작해야 한다.
|
||||
expect(mainClass).toContain("h-screen");
|
||||
expect(mainClass).not.toContain("min-h-screen");
|
||||
expect(mainClass).toContain("flex");
|
||||
expect(mainClass).toContain("overflow-hidden");
|
||||
|
||||
// 좌측 BlocksSidebar 는 자체 스크롤(overflow-y-auto)을 가져야 한다.
|
||||
const blocksSidebar = screen.getByTestId("blocks-sidebar") as HTMLElement;
|
||||
const blocksClass = blocksSidebar.getAttribute("class") ?? "";
|
||||
expect(blocksClass).toContain("overflow-y-auto");
|
||||
expect(blocksClass).toContain("pb-scroll");
|
||||
|
||||
// 중앙 EditorCanvas 는 자체 스크롤(overflow-auto)을 가져야 한다.
|
||||
const canvas = screen.getByTestId("editor-canvas") as HTMLElement;
|
||||
const canvasClass = canvas.getAttribute("class") ?? "";
|
||||
expect(canvasClass).toContain("overflow-auto");
|
||||
expect(canvasClass).toContain("pb-scroll");
|
||||
|
||||
// 우측 PropertiesSidebar 도 자체 스크롤(overflow-auto)을 가져야 한다.
|
||||
const propsSidebar = screen.getByTestId("properties-sidebar") as HTMLElement;
|
||||
const propsClass = propsSidebar.getAttribute("class") ?? "";
|
||||
expect(propsClass).toContain("overflow-auto");
|
||||
expect(propsClass).toContain("pb-scroll");
|
||||
});
|
||||
|
||||
it("섹션 자식 블록을 선택해도 섹션 wrapper 가 강조되어야 한다", () => {
|
||||
const section: Block = {
|
||||
id: "sec_1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, cleanup, within } from "@testing-library/react";
|
||||
import { FormControllerPanel } from "@/app/editor/forms/FormControllerPanel";
|
||||
import type { Block, FormBlockProps } from "@/features/editor/state/editorStore";
|
||||
|
||||
@@ -222,4 +222,183 @@ describe("FormControllerPanel", () => {
|
||||
expect(scriptTextarea.value).toContain("params.email || \"\"");
|
||||
expect(scriptTextarea.value).toContain("new Date()");
|
||||
});
|
||||
|
||||
it("폼 컨트롤러 주요 인풋/셀렉트 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const formBlock = makeFormBlock("form-theme", {
|
||||
submitTarget: "webhook",
|
||||
} as any);
|
||||
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<FormControllerPanel
|
||||
block={formBlock}
|
||||
blocks={[formBlock]}
|
||||
selectedBlockId="form-theme"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const submitTargetSelect = screen.getByLabelText("전송 대상") as HTMLSelectElement;
|
||||
const submitTargetClass = submitTargetSelect.getAttribute("class") ?? "";
|
||||
expect(submitTargetClass).toContain("bg-white");
|
||||
expect(submitTargetClass).toContain("text-slate-900");
|
||||
expect(submitTargetClass).toContain("border-slate-300");
|
||||
expect(submitTargetClass).toContain("dark:bg-slate-900");
|
||||
expect(submitTargetClass).toContain("dark:text-slate-100");
|
||||
expect(submitTargetClass).toContain("dark:border-slate-700");
|
||||
|
||||
const formWidthModeSelect = screen.getByLabelText("폼 너비 모드") as HTMLSelectElement;
|
||||
const formWidthModeClass = formWidthModeSelect.getAttribute("class") ?? "";
|
||||
expect(formWidthModeClass).toContain("bg-white");
|
||||
expect(formWidthModeClass).toContain("text-slate-900");
|
||||
expect(formWidthModeClass).toContain("border-slate-300");
|
||||
expect(formWidthModeClass).toContain("dark:bg-slate-900");
|
||||
expect(formWidthModeClass).toContain("dark:text-slate-100");
|
||||
expect(formWidthModeClass).toContain("dark:border-slate-700");
|
||||
|
||||
const successInput = screen.getByLabelText("성공 메시지") as HTMLInputElement;
|
||||
const successClass = successInput.getAttribute("class") ?? "";
|
||||
expect(successClass).toContain("bg-white");
|
||||
expect(successClass).toContain("text-slate-900");
|
||||
expect(successClass).toContain("border-slate-300");
|
||||
expect(successClass).toContain("dark:bg-slate-900");
|
||||
expect(successClass).toContain("dark:text-slate-100");
|
||||
expect(successClass).toContain("dark:border-slate-700");
|
||||
|
||||
const errorInput = screen.getByLabelText("에러 메시지") as HTMLInputElement;
|
||||
const errorClass = errorInput.getAttribute("class") ?? "";
|
||||
expect(errorClass).toContain("bg-white");
|
||||
expect(errorClass).toContain("text-slate-900");
|
||||
expect(errorClass).toContain("border-slate-300");
|
||||
expect(errorClass).toContain("dark:bg-slate-900");
|
||||
expect(errorClass).toContain("dark:text-slate-100");
|
||||
expect(errorClass).toContain("dark:border-slate-700");
|
||||
|
||||
const extraParamsTextarea = screen.getByLabelText(/추가 파라미터/) as HTMLTextAreaElement;
|
||||
const extraParamsClass = extraParamsTextarea.getAttribute("class") ?? "";
|
||||
expect(extraParamsClass).toContain("bg-white");
|
||||
expect(extraParamsClass).toContain("text-slate-900");
|
||||
expect(extraParamsClass).toContain("border-slate-300");
|
||||
expect(extraParamsClass).toContain("dark:bg-slate-900");
|
||||
expect(extraParamsClass).toContain("dark:text-slate-100");
|
||||
expect(extraParamsClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
|
||||
it("Google Sheets 연동 가이드 모달 카드와 textarea 는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const formBlock = makeFormBlock("form-webhook-theme", {
|
||||
submitTarget: "webhook",
|
||||
} as any);
|
||||
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<FormControllerPanel
|
||||
block={formBlock}
|
||||
blocks={[formBlock]}
|
||||
selectedBlockId="form-webhook-theme"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const guideButton = screen.getByRole("button", { name: "Google Sheets 연동 가이드" });
|
||||
fireEvent.click(guideButton);
|
||||
|
||||
const heading = screen.getByRole("heading", { name: "Google Sheets 연동 가이드" });
|
||||
const card = heading.closest("div")?.parentElement as HTMLDivElement | null;
|
||||
expect(card).not.toBeNull();
|
||||
|
||||
const cardClass = card!.getAttribute("class") ?? "";
|
||||
expect(cardClass).toContain("bg-white");
|
||||
expect(cardClass).toContain("text-slate-900");
|
||||
expect(cardClass).toContain("border-slate-200");
|
||||
expect(cardClass).toContain("dark:bg-slate-900");
|
||||
expect(cardClass).toContain("dark:text-slate-100");
|
||||
expect(cardClass).toContain("dark:border-slate-700");
|
||||
|
||||
const scriptTextarea = screen.getByDisplayValue(/function doPost/) as HTMLTextAreaElement;
|
||||
const scriptClass = scriptTextarea.getAttribute("class") ?? "";
|
||||
expect(scriptClass).toContain("bg-white");
|
||||
expect(scriptClass).toContain("text-slate-900");
|
||||
expect(scriptClass).toContain("border-slate-300");
|
||||
expect(scriptClass).toContain("dark:bg-slate-900");
|
||||
expect(scriptClass).toContain("dark:text-slate-100");
|
||||
expect(scriptClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
|
||||
it("폼 필드 매핑 체크박스와 필수 체크박스는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const formBlock = makeFormBlock("form-fields-theme", {
|
||||
fieldIds: ["input-1"],
|
||||
requiredFieldIds: ["input-1"],
|
||||
} as any);
|
||||
|
||||
const inputBlock: Block = {
|
||||
id: "input-1",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "이름",
|
||||
formFieldName: "name",
|
||||
} as any,
|
||||
} as any;
|
||||
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<FormControllerPanel
|
||||
block={formBlock}
|
||||
blocks={[formBlock, inputBlock]}
|
||||
selectedBlockId="form-fields-theme"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fieldset = screen.getByRole("group", { name: "폼 필드 매핑" });
|
||||
const checkboxes = within(fieldset).getAllByRole("checkbox");
|
||||
|
||||
const includeCheckbox = checkboxes[0] as HTMLInputElement;
|
||||
const requiredCheckbox = checkboxes[1] as HTMLInputElement;
|
||||
|
||||
for (const checkbox of [includeCheckbox, requiredCheckbox]) {
|
||||
const className = checkbox.getAttribute("class") ?? "";
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("border-slate-300");
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:border-slate-600");
|
||||
}
|
||||
});
|
||||
|
||||
it("Submit 버튼 셀렉트는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const formBlock = makeFormBlock("form-submit-theme", {
|
||||
submitButtonId: "btn-1",
|
||||
} as any);
|
||||
|
||||
const buttonBlock: Block = {
|
||||
id: "btn-1",
|
||||
type: "button",
|
||||
props: {
|
||||
label: "제출하기",
|
||||
} as any,
|
||||
} as any;
|
||||
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<FormControllerPanel
|
||||
block={formBlock}
|
||||
blocks={[formBlock, buttonBlock]}
|
||||
selectedBlockId="form-submit-theme"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("Submit 버튼") 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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,6 +104,56 @@ describe("FormInputPropertiesPanel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("텍스트 색상 팔레트에서 \"없음\" 을 선택하면 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("색상 팔레트");
|
||||
fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement);
|
||||
|
||||
// 팔레트에서 "없음" 항목을 버튼 role 기준으로 선택한다.
|
||||
const noneButtons = screen.getAllByRole("button", { name: "없음" });
|
||||
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("필드 텍스트 색상 HEX") as HTMLInputElement;
|
||||
expect(hexInput.value).toBe("");
|
||||
});
|
||||
|
||||
it("필드 너비 셀렉트 변경 시 updateBlock 이 widthMode 로 호출되어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
@@ -266,6 +316,67 @@ describe("FormInputPropertiesPanel", () => {
|
||||
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("라벨 타입") 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("필드 라벨") 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("라벨 표시 방식") 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("필드 타입") 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", () => {
|
||||
@@ -679,6 +790,91 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
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("라벨 타입") 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("필드 라벨") 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("라벨 표시 방식") 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("필드 너비") 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("라벨") 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("라벨") 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", () => {
|
||||
@@ -1042,6 +1238,32 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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("필드 너비") 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();
|
||||
|
||||
@@ -1452,4 +1674,65 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
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("그룹 타이틀") 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("그룹 타이틀 표시 방식") 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("옵션 레이아웃") 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("전송 키") 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("라벨") 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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -192,4 +192,43 @@ describe("ImagePropertiesPanel", () => {
|
||||
expect.objectContaining({ borderRadius: "sm", borderRadiusPx: 24 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("ImagePropertiesPanel 주요 인풋/셀렉트 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ImagePropertiesPanel
|
||||
imageProps={baseProps as any}
|
||||
selectedBlockId="image-theme-1"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const sourceSelect = screen.getByLabelText("이미지 소스") as HTMLSelectElement;
|
||||
const sourceClass = sourceSelect.getAttribute("class") ?? "";
|
||||
expect(sourceClass).toContain("bg-white");
|
||||
expect(sourceClass).toContain("text-slate-900");
|
||||
expect(sourceClass).toContain("border-slate-300");
|
||||
expect(sourceClass).toContain("dark:bg-slate-900");
|
||||
expect(sourceClass).toContain("dark:text-slate-100");
|
||||
expect(sourceClass).toContain("dark:border-slate-700");
|
||||
|
||||
const urlInput = screen.getByLabelText("이미지 URL") as HTMLInputElement;
|
||||
const urlClass = urlInput.getAttribute("class") ?? "";
|
||||
expect(urlClass).toContain("bg-white");
|
||||
expect(urlClass).toContain("text-slate-900");
|
||||
expect(urlClass).toContain("border-slate-300");
|
||||
expect(urlClass).toContain("dark:bg-slate-900");
|
||||
expect(urlClass).toContain("dark:text-slate-100");
|
||||
expect(urlClass).toContain("dark:border-slate-700");
|
||||
|
||||
const alignSelect = screen.getByLabelText("이미지 정렬") as HTMLSelectElement;
|
||||
const alignClass = alignSelect.getAttribute("class") ?? "";
|
||||
expect(alignClass).toContain("bg-white");
|
||||
expect(alignClass).toContain("text-slate-900");
|
||||
expect(alignClass).toContain("border-slate-300");
|
||||
expect(alignClass).toContain("dark:bg-slate-900");
|
||||
expect(alignClass).toContain("dark:text-slate-100");
|
||||
expect(alignClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,6 +121,48 @@ describe("ListPropertiesPanel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("텍스트 색상 팔레트에서 \"없음\" 을 선택하면 textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ListPropertiesPanel
|
||||
listProps={{ ...baseProps, textColorCustom: "#ff0000" }}
|
||||
selectedBlockId="list-text-none"
|
||||
selectedListItemId={null}
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 첫 번째 ColorPickerField(텍스트 색상)의 팔레트 버튼을 연다.
|
||||
const paletteButtons = screen.getAllByText("색상 팔레트");
|
||||
fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement);
|
||||
|
||||
// 팔레트에서 "없음" 항목을 버튼 role 기준으로 선택한다 (불릿 스타일 select 의 option "없음" 과 구분).
|
||||
const noneButtons = screen.getAllByRole("button", { name: "없음" });
|
||||
fireEvent.click(noneButtons[0]);
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
"list-text-none",
|
||||
expect.objectContaining({ textColorCustom: "" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("textColorCustom 이 비어 있으면 텍스트 색상 HEX 인풋 값은 빈 문자열이어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ListPropertiesPanel
|
||||
listProps={{ ...baseProps, textColorCustom: "" }}
|
||||
selectedBlockId="list-text-empty"
|
||||
selectedListItemId={null}
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("리스트 텍스트 색상 HEX") as HTMLInputElement;
|
||||
expect(hexInput.value).toBe("");
|
||||
});
|
||||
|
||||
it("불릿 스타일 셀렉트 변경 시 bulletStyle 과 ordered 가 함께 업데이트되어야 한다 (decimal)", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
@@ -183,4 +225,73 @@ describe("ListPropertiesPanel", () => {
|
||||
expect.objectContaining({ gapYPx: 16 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("리스트 정렬 셀렉트는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ListPropertiesPanel
|
||||
listProps={baseProps}
|
||||
selectedBlockId="list-align-theme"
|
||||
selectedListItemId={null}
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("리스트 정렬") 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("리스트 불릿 스타일 셀렉트는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ListPropertiesPanel
|
||||
listProps={baseProps}
|
||||
selectedBlockId="list-bullet-theme"
|
||||
selectedListItemId={null}
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("리스트 불릿 스타일") 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("리스트 아이템 textarea 는 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<ListPropertiesPanel
|
||||
listProps={baseProps}
|
||||
selectedBlockId="list-theme"
|
||||
selectedListItemId={null}
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const textarea = screen.getByLabelText("리스트 아이템들") as HTMLTextAreaElement;
|
||||
const className = textarea.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 다크 배경 + 밝은 텍스트
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,4 +147,84 @@ describe("LoginPage", () => {
|
||||
expect(pushMock).toHaveBeenCalledWith("/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
it("로그인 페이지는 전역 라이트/다크 테마 배경 클래스를 사용해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
const main = await screen.findByRole("main");
|
||||
const className = (main as HTMLElement).className;
|
||||
|
||||
expect(className).toContain("bg-slate-100");
|
||||
expect(className).toContain("dark:bg-slate-950");
|
||||
});
|
||||
|
||||
it("비밀번호 입력 필드는 이메일 입력과 동일한 라이트/다크 테마 클래스를 사용해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
|
||||
const emailClass = emailInput.className;
|
||||
const passwordClass = passwordInput.className;
|
||||
|
||||
expect(emailClass).toContain("border-slate-300");
|
||||
expect(emailClass).toContain("bg-white");
|
||||
expect(emailClass).toContain("text-slate-900");
|
||||
expect(emailClass).toContain("dark:border-slate-700");
|
||||
expect(emailClass).toContain("dark:bg-slate-950");
|
||||
expect(emailClass).toContain("dark:text-slate-50");
|
||||
|
||||
expect(passwordClass).toBe(emailClass);
|
||||
});
|
||||
|
||||
it("로그인 페이지에도 테마 전환 버튼이 있고 클릭 시 html 요소의 dark 클래스가 토글되어야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock.mock.calls[0][0]).toBe("/api/auth/me");
|
||||
});
|
||||
|
||||
const html = document.documentElement;
|
||||
html.classList.remove("dark");
|
||||
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "테마 전환" });
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
|
||||
themeToggleButton.click();
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(true);
|
||||
|
||||
themeToggleButton.click();
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
|
||||
|
||||
describe("NumericPropertyControl - 테마", () => {
|
||||
it("프리셋 셀렉트는 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(
|
||||
<NumericPropertyControl
|
||||
label="테스트 숫자"
|
||||
unitLabel="px"
|
||||
value={16}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "small", label: "작게", value: 12 },
|
||||
{ id: "medium", label: "보통", value: 16 },
|
||||
]}
|
||||
onChangeValue={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("테스트 숫자 프리셋") as HTMLSelectElement;
|
||||
const className = select.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 다크 배경 + 밝은 텍스트
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
});
|
||||
@@ -134,4 +134,16 @@ describe("PreviewPage - canvasPreset / canvasWidthPx", () => {
|
||||
expect(inner.style.backgroundColor).toBe("rgb(17, 17, 17)");
|
||||
expect(main.style.backgroundColor).toBe("rgb(34, 34, 34)");
|
||||
});
|
||||
|
||||
it("bodyBgColorHex 가 빈 문자열이면 PreviewPage main 에 background-color 가 들어가지 않아야 한다", () => {
|
||||
mockState.projectConfig = {
|
||||
...(mockState.projectConfig as ProjectConfig),
|
||||
bodyBgColorHex: "",
|
||||
} as ProjectConfig;
|
||||
|
||||
const { container } = render(<PreviewPage />);
|
||||
const main = container.querySelector("main") as HTMLElement;
|
||||
// 명시적으로 빈 문자열을 지정한 경우에는 PreviewPage 가 어떤 기본 배경색도 강제로 설정하지 않아야 한다.
|
||||
expect(main.style.backgroundColor).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,5 +122,46 @@ describe("ProjectPropertiesPanel", () => {
|
||||
fireEvent.change(input, { target: { value: "#654321" } });
|
||||
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledWith({ bodyBgColorHex: "#654321" });
|
||||
});
|
||||
|
||||
it("캔버스 배경색 팔레트에서 \"없음\" 을 선택하면 canvasBgColorHex 가 빈 문자열로 업데이트되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
// 첫 번째 ColorPickerField(캔버스 배경색)의 팔레트 버튼을 연다.
|
||||
const paletteButtons = screen.getAllByText("색상 팔레트");
|
||||
fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement);
|
||||
|
||||
// 팔레트에서 "없음" 항목을 선택한다.
|
||||
const noneButton = screen.getByText("없음");
|
||||
fireEvent.click(noneButton);
|
||||
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledWith({ canvasBgColorHex: "" });
|
||||
});
|
||||
|
||||
it("페이지 배경색 팔레트에서 \"없음\" 을 선택하면 bodyBgColorHex 가 빈 문자열로 업데이트되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
// 두 번째 ColorPickerField(페이지 배경색)의 팔레트 버튼은 기본 선택 팔레트 라벨("어두운 텍스트")를 기준으로 찾는다.
|
||||
const darkLabel = screen.getByText("어두운 텍스트");
|
||||
fireEvent.click(darkLabel.closest("button") as HTMLButtonElement);
|
||||
|
||||
const noneButton = screen.getByText("없음");
|
||||
fireEvent.click(noneButton);
|
||||
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledWith({ bodyBgColorHex: "" });
|
||||
});
|
||||
|
||||
it("프로젝트 제목 입력은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const input = screen.getByLabelText("프로젝트 제목") as HTMLInputElement;
|
||||
const className = input.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 다크 배경 + 밝은 텍스트
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("ProjectSubmissionsPage - 폼 제출 내역", () => {
|
||||
expect(errorText).toBeTruthy();
|
||||
});
|
||||
|
||||
it("상단에 '프로젝트 목록' 링크가 있고 클릭 시 /projects 로 이동해야 한다", async () => {
|
||||
it("상단에 '프로젝트 목록' 링크가 있고 href 가 /projects 이어야 한다", async () => {
|
||||
useParamsMock.mockReturnValue({ slug: "test-project" });
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
@@ -151,11 +151,99 @@ describe("ProjectSubmissionsPage - 폼 제출 내역", () => {
|
||||
|
||||
const backLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
expect(backLink).toBeTruthy();
|
||||
expect(backLink.getAttribute("href")).toBe("/projects");
|
||||
});
|
||||
|
||||
backLink.click();
|
||||
it("상단 헤더에 공통 GNB와 테마 전환 버튼이 노출되고 테마 토글이 html 요소의 dark 클래스를 토글해야 한다", async () => {
|
||||
useParamsMock.mockReturnValue({ slug: "test-project" });
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify([]), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<ProjectSubmissionsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(pushMock).toHaveBeenCalledWith("/projects");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const dashboardLink = await screen.findByRole("link", { name: "대시보드" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "전체 제출 내역" });
|
||||
|
||||
expect(dashboardLink.getAttribute("href")).toBe("/dashboard");
|
||||
expect(projectsLink.getAttribute("href")).toBe("/projects");
|
||||
expect(submissionsLink.getAttribute("href")).toBe("/projects/submissions");
|
||||
|
||||
const html = document.documentElement;
|
||||
html.classList.remove("dark");
|
||||
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "테마 전환" });
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
|
||||
themeToggleButton.click();
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(true);
|
||||
|
||||
themeToggleButton.click();
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
});
|
||||
|
||||
it("프로젝트별 제출 내역 테이블은 라이트 테마에서 읽기 쉬운 텍스트 색상을 사용해야 한다", async () => {
|
||||
useParamsMock.mockReturnValue({ slug: "test-project" });
|
||||
|
||||
const submissions = [
|
||||
{
|
||||
id: "1",
|
||||
createdAt: "2025-01-01T12:00:00.000Z",
|
||||
projectSlug: "test-project",
|
||||
payload: {
|
||||
name: "홍길동",
|
||||
email: "user@example.com",
|
||||
phone: "010-1234-5678",
|
||||
birthdate: "1990-01-01",
|
||||
message: "안녕하세요",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(submissions), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
const { container } = render(<ProjectSubmissionsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const headerRow = container.querySelector("thead tr") as HTMLElement | null;
|
||||
expect(headerRow).not.toBeNull();
|
||||
|
||||
const headerClass = headerRow!.className;
|
||||
expect(headerClass).toContain("border-slate-200");
|
||||
expect(headerClass).toContain("text-slate-600");
|
||||
expect(headerClass).toContain("dark:border-slate-800");
|
||||
expect(headerClass).toContain("dark:text-slate-400");
|
||||
|
||||
const nameCellNode = await screen.findByText("홍길동");
|
||||
const nameCell = nameCellNode.closest("td") as HTMLElement | null;
|
||||
expect(nameCell).not.toBeNull();
|
||||
|
||||
const nameClass = nameCell!.className;
|
||||
expect(nameClass).toContain("text-slate-900");
|
||||
expect(nameClass).toContain("dark:text-slate-100");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,6 +144,114 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(link.closest("a")?.getAttribute("href")).toBe("/dashboard");
|
||||
});
|
||||
|
||||
it("프로젝트 목록 테이블은 라이트/다크 테마에서 읽기 쉬운 텍스트 색상을 사용해야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const titleCellNode = await screen.findByText("테스트 프로젝트 A");
|
||||
const titleCell = titleCellNode.closest("td") as HTMLElement | null;
|
||||
expect(titleCell).not.toBeNull();
|
||||
|
||||
const titleClassName = titleCell!.className;
|
||||
expect(titleClassName).toContain("text-slate-900");
|
||||
expect(titleClassName).toContain("dark:text-slate-100");
|
||||
|
||||
const slugCellNode = screen.getByText("test-project-a");
|
||||
const slugCell = slugCellNode.closest("td") as HTMLElement | null;
|
||||
expect(slugCell).not.toBeNull();
|
||||
|
||||
const slugClassName = slugCell!.className;
|
||||
expect(slugClassName).toContain("text-slate-600");
|
||||
expect(slugClassName).toContain("dark:text-slate-300");
|
||||
});
|
||||
|
||||
it("상단 툴바/액션 링크/페이지네이션은 라이트/다크 테마 모두에서 읽기 쉬운 색상 클래스를 사용해야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const toolbar = await screen.findByTestId("projects-toolbar");
|
||||
const toolbarClass = toolbar.className;
|
||||
expect(toolbarClass).toContain("text-slate-600");
|
||||
expect(toolbarClass).toContain("dark:text-slate-300");
|
||||
|
||||
const editLink = screen.getByText("편집").closest("a") as HTMLElement | null;
|
||||
expect(editLink).not.toBeNull();
|
||||
const editClass = editLink!.className;
|
||||
expect(editClass).toContain("text-sky-600");
|
||||
expect(editClass).toContain("dark:text-sky-300");
|
||||
|
||||
const previewLink = screen.getByText("미리보기").closest("a") as HTMLElement | null;
|
||||
expect(previewLink).not.toBeNull();
|
||||
const previewClass = previewLink!.className;
|
||||
expect(previewClass).toContain("text-slate-600");
|
||||
expect(previewClass).toContain("dark:text-slate-300");
|
||||
|
||||
const submissionsLink = screen.getByText("폼 제출 내역").closest("a") as HTMLElement | null;
|
||||
expect(submissionsLink).not.toBeNull();
|
||||
const submissionsClass = submissionsLink!.className;
|
||||
expect(submissionsClass).toContain("text-emerald-600");
|
||||
expect(submissionsClass).toContain("dark:text-emerald-300");
|
||||
|
||||
const deleteButton = screen.getByText("삭제").closest("button") as HTMLElement | null;
|
||||
expect(deleteButton).not.toBeNull();
|
||||
const deleteClass = deleteButton!.className;
|
||||
expect(deleteClass).toContain("text-red-600");
|
||||
expect(deleteClass).toContain("dark:text-red-300");
|
||||
|
||||
const prevButton = screen.getByRole("button", { name: "이전" });
|
||||
const prevClass = prevButton.className;
|
||||
expect(prevClass).toContain("border-slate-300");
|
||||
expect(prevClass).toContain("text-slate-700");
|
||||
expect(prevClass).toContain("dark:border-slate-700");
|
||||
expect(prevClass).toContain("dark:text-slate-300");
|
||||
});
|
||||
|
||||
it("GNB에서 현재 페이지인 '프로젝트 목록' 탭에 aria-current=\"page\"가 설정되어야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
|
||||
@@ -56,6 +56,30 @@ describe("PropertiesSidebar HELP 튜토리얼 모달", () => {
|
||||
expect(screen.getByText(/제목과 본문 텍스트를 입력할 때 사용하는 기본 블록입니다/)).toBeDefined();
|
||||
});
|
||||
|
||||
it("HELP 모달은 라이트/다크 테마에 맞는 카드 배경/테두리/텍스트 클래스를 사용해야 한다", () => {
|
||||
const textBlock = createTextBlock();
|
||||
|
||||
render(<PropertiesSidebar {...defaultProps([textBlock], textBlock.id)} />);
|
||||
|
||||
const helpButton = screen.getByRole("button", { name: "HELP" });
|
||||
fireEvent.click(helpButton);
|
||||
|
||||
// 모달 카드 컨테이너 (제목 h3 → 헤더 div → 카드 div 순으로 감싸져 있으므로 parentElement.parentElement 사용)
|
||||
const heading = screen.getByText("텍스트 블록 튜토리얼");
|
||||
const modalCard = heading.parentElement?.parentElement as HTMLElement | null;
|
||||
expect(modalCard).not.toBeNull();
|
||||
const className = modalCard!.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트 + 연한 테두리
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
expect(className).toContain("border-slate-200");
|
||||
// 다크 모드: 기존 다크 톤 유지
|
||||
expect(className).toContain("dark:border-slate-700");
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("모달의 닫기 버튼을 클릭하면 튜토리얼 모달이 닫혀야 한다", () => {
|
||||
const textBlock = createTextBlock();
|
||||
|
||||
@@ -69,4 +93,65 @@ describe("PropertiesSidebar HELP 튜토리얼 모달", () => {
|
||||
|
||||
expect(screen.queryByText("텍스트 블록 튜토리얼")).toBeNull();
|
||||
});
|
||||
|
||||
it("텍스트 블록이 선택된 상태에서 상단 액션 버튼들은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
const textBlock = createTextBlock();
|
||||
|
||||
render(<PropertiesSidebar {...defaultProps([textBlock], textBlock.id)} />);
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: "블록 삭제" });
|
||||
const duplicateButton = screen.getByRole("button", { name: "블록 복제" });
|
||||
const helpButton = screen.getByRole("button", { name: "HELP" });
|
||||
|
||||
for (const btn of [deleteButton, duplicateButton, helpButton]) {
|
||||
const className = btn.getAttribute("class") ?? "";
|
||||
// 라이트 모드: 밝은 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-slate-50");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 기존 다크 톤 유지
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
}
|
||||
});
|
||||
|
||||
it("텍스트 블록이 선택된 상태에서 내용 편집 textarea는 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
const textBlock = createTextBlock();
|
||||
|
||||
render(<PropertiesSidebar {...defaultProps([textBlock], textBlock.id)} />);
|
||||
|
||||
const textarea = screen.getByLabelText("선택한 텍스트 블록 내용") as HTMLTextAreaElement;
|
||||
const className = textarea.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 다크 배경 + 밝은 텍스트
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("속성 패널 루트는 라이트/다크 테마에 맞는 배경/테두리 클래스를 사용해야 한다", () => {
|
||||
const props = defaultProps([], null);
|
||||
|
||||
render(<PropertiesSidebar {...props} />);
|
||||
|
||||
const aside = screen.getByTestId("properties-sidebar") as HTMLElement;
|
||||
const className = aside.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("border-slate-200");
|
||||
expect(className).toContain("dark:border-slate-800");
|
||||
expect(className).toContain("dark:bg-slate-950/40");
|
||||
});
|
||||
|
||||
it("속성 패널 루트는 pb-scroll 커스텀 스크롤바 클래스를 사용하지 않아야 한다", () => {
|
||||
const props = defaultProps([], null);
|
||||
|
||||
render(<PropertiesSidebar {...props} />);
|
||||
|
||||
const aside = screen.getByTestId("properties-sidebar") as HTMLElement;
|
||||
const className = aside.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).not.toContain("pb-scroll");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { PropertySliderField } from "@/features/editor/components/PropertySliderField";
|
||||
|
||||
describe("PropertySliderField - 테마", () => {
|
||||
it("텍스트 입력은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(
|
||||
<PropertySliderField
|
||||
label="테스트 슬라이더"
|
||||
ariaLabelSlider="테스트 슬라이더"
|
||||
ariaLabelInput="테스트 값 입력"
|
||||
value={10}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("테스트 값 입력") as HTMLInputElement;
|
||||
const className = input.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
expect(className).toContain("bg-white");
|
||||
expect(className).toContain("text-slate-900");
|
||||
// 다크 모드: 다크 배경 + 밝은 텍스트
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ describe("PublicPageRenderer - 배경색 위임", () => {
|
||||
const className = root!.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).not.toContain("bg-slate-950");
|
||||
expect(className).not.toContain("text-slate-50");
|
||||
});
|
||||
|
||||
it("루트 텍스트/버튼 블록 영역은 고정 bg-slate-950 배경 대신 캔버스 배경을 그대로 보여야 한다", () => {
|
||||
|
||||
@@ -111,6 +111,24 @@ describe("PublicPageRenderer - 폼 필드 스타일", () => {
|
||||
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[] = [
|
||||
{
|
||||
@@ -201,6 +219,27 @@ describe("PublicPageRenderer - 폼 필드 스타일", () => {
|
||||
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[] = [
|
||||
{
|
||||
@@ -338,28 +377,86 @@ describe("PublicPageRenderer - 폼 필드 스타일", () => {
|
||||
|
||||
const { getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
|
||||
|
||||
const group = getByTestId("preview-form-checkbox-group") as HTMLElement;
|
||||
const firstLabel = getFirstLabel(group);
|
||||
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");
|
||||
// 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");
|
||||
});
|
||||
// 옵션 컨테이너 스타일: 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 블록의 옵션 input 은 type/name/value 및 FormBlock.requiredFieldIds 기반 required 를 가져야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_ctrl_radio",
|
||||
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",
|
||||
@@ -427,6 +524,29 @@ describe("PublicPageRenderer - 폼 필드 스타일", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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[] = [
|
||||
{
|
||||
@@ -547,6 +667,52 @@ describe("PublicPageRenderer - 폼 필드 스타일", () => {
|
||||
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[] = [
|
||||
{
|
||||
|
||||
@@ -101,4 +101,47 @@ describe("PublicPageRenderer - 섹션/루트 레이아웃", () => {
|
||||
const innerContainer = rootSection.querySelector(".pb-root-inner");
|
||||
expect(innerContainer).not.toBeNull();
|
||||
});
|
||||
|
||||
it("블록 배열 순서를 따라 루트 블록과 섹션 블록이 섞여 렌더되어야 한다", () => {
|
||||
const rootBeforeProps: TextBlockProps = {
|
||||
text: "루트1",
|
||||
align: "left",
|
||||
size: "base",
|
||||
} as TextBlockProps;
|
||||
|
||||
const rootAfterProps: TextBlockProps = {
|
||||
text: "루트2",
|
||||
align: "left",
|
||||
size: "base",
|
||||
} as TextBlockProps;
|
||||
|
||||
const rootBefore: Block = {
|
||||
id: "root_order_before",
|
||||
type: "text",
|
||||
props: rootBeforeProps,
|
||||
} as any;
|
||||
|
||||
const sectionWithText = makeSectionWithText();
|
||||
|
||||
const rootAfter: Block = {
|
||||
id: "root_order_after",
|
||||
type: "text",
|
||||
props: rootAfterProps,
|
||||
} as any;
|
||||
|
||||
const blocks: Block[] = [rootBefore, ...sectionWithText, rootAfter];
|
||||
|
||||
const { container } = render(<PublicPageRenderer blocks={blocks} />);
|
||||
|
||||
const sections = Array.from(container.querySelectorAll("section"));
|
||||
|
||||
expect(sections.length).toBe(3);
|
||||
expect(sections[0].className).toContain("pb-root");
|
||||
expect(sections[1].className).toContain("pb-section");
|
||||
expect(sections[2].className).toContain("pb-root");
|
||||
|
||||
expect(sections[0].textContent).toContain("루트1");
|
||||
expect(sections[1].textContent).toContain("섹션 레이아웃 텍스트");
|
||||
expect(sections[2].textContent).toContain("루트2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -363,4 +363,98 @@ describe("SectionPropertiesPanel", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("SectionPropertiesPanel 배경 이미지/비디오 소스 및 URL 인풋은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<SectionPropertiesPanel
|
||||
sectionProps={{
|
||||
...baseProps,
|
||||
backgroundImageSrc: "https://example.com/bg.png",
|
||||
backgroundImageSourceType: "externalUrl",
|
||||
backgroundVideoSrc: "https://example.com/bg-video.mp4",
|
||||
backgroundVideoSourceType: "externalUrl",
|
||||
} as any}
|
||||
selectedBlockId="section-theme-1"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const bgImageSourceSelect = screen.getByLabelText("배경 이미지 소스") as HTMLSelectElement;
|
||||
const bgImageSourceClass = bgImageSourceSelect.getAttribute("class") ?? "";
|
||||
expect(bgImageSourceClass).toContain("bg-white");
|
||||
expect(bgImageSourceClass).toContain("text-slate-900");
|
||||
expect(bgImageSourceClass).toContain("border-slate-300");
|
||||
expect(bgImageSourceClass).toContain("dark:bg-slate-900");
|
||||
expect(bgImageSourceClass).toContain("dark:text-slate-100");
|
||||
expect(bgImageSourceClass).toContain("dark:border-slate-700");
|
||||
|
||||
const bgImageUrlInput = screen.getByLabelText("배경 이미지 URL") as HTMLInputElement;
|
||||
const bgImageUrlClass = bgImageUrlInput.getAttribute("class") ?? "";
|
||||
expect(bgImageUrlClass).toContain("bg-white");
|
||||
expect(bgImageUrlClass).toContain("text-slate-900");
|
||||
expect(bgImageUrlClass).toContain("border-slate-300");
|
||||
expect(bgImageUrlClass).toContain("dark:bg-slate-900");
|
||||
expect(bgImageUrlClass).toContain("dark:text-slate-100");
|
||||
expect(bgImageUrlClass).toContain("dark:border-slate-700");
|
||||
|
||||
const bgVideoUrlInput = screen.getByLabelText("배경 비디오 URL") as HTMLInputElement;
|
||||
const bgVideoUrlClass = bgVideoUrlInput.getAttribute("class") ?? "";
|
||||
expect(bgVideoUrlClass).toContain("bg-white");
|
||||
expect(bgVideoUrlClass).toContain("text-slate-900");
|
||||
expect(bgVideoUrlClass).toContain("border-slate-300");
|
||||
expect(bgVideoUrlClass).toContain("dark:bg-slate-900");
|
||||
expect(bgVideoUrlClass).toContain("dark:text-slate-100");
|
||||
expect(bgVideoUrlClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
|
||||
it("섹션 레이아웃 및 배경 이미지 레이아웃 셀렉트들은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<SectionPropertiesPanel
|
||||
sectionProps={{
|
||||
...baseProps,
|
||||
columns: [
|
||||
{ id: "col_1", span: 6 },
|
||||
{ id: "col_2", span: 6 },
|
||||
],
|
||||
alignItems: "top",
|
||||
backgroundImageSrc: "https://example.com/bg.png",
|
||||
backgroundImageSourceType: "externalUrl",
|
||||
backgroundImagePositionMode: "preset",
|
||||
backgroundImageSize: "cover",
|
||||
backgroundImagePosition: "center",
|
||||
backgroundImageRepeat: "no-repeat",
|
||||
} as any}
|
||||
selectedBlockId="section-theme-layout-1"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const columnLayoutSelect = screen.getByLabelText("섹션 컬럼 레이아웃") as HTMLSelectElement;
|
||||
const alignItemsSelect = screen.getByLabelText("섹션 컬럼 세로 정렬") as HTMLSelectElement;
|
||||
const positionModeSelect = screen.getByLabelText("배경 이미지 위치 모드") as HTMLSelectElement;
|
||||
const sizeSelect = screen.getByLabelText("배경 이미지 크기") as HTMLSelectElement;
|
||||
const positionSelect = screen.getByLabelText("배경 이미지 위치") as HTMLSelectElement;
|
||||
const repeatSelect = screen.getByLabelText("배경 이미지 반복") as HTMLSelectElement;
|
||||
|
||||
const assertDualTheme = (el: HTMLElement) => {
|
||||
const className = el.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");
|
||||
};
|
||||
|
||||
assertDualTheme(columnLayoutSelect);
|
||||
assertDualTheme(alignItemsSelect);
|
||||
assertDualTheme(positionModeSelect);
|
||||
assertDualTheme(sizeSelect);
|
||||
assertDualTheme(positionSelect);
|
||||
assertDualTheme(repeatSelect);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,10 +56,12 @@ describe("SignupPage", () => {
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("비밀번호 확인") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "회원가입" });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "new@example.com" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "securePass1" } });
|
||||
fireEvent.change(confirmInput, { target: { value: "securePass1" } });
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
@@ -118,10 +120,12 @@ describe("SignupPage", () => {
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("비밀번호 확인") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "회원가입" });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "dup@example.com" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "securePass1" } });
|
||||
fireEvent.change(confirmInput, { target: { value: "securePass1" } });
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
@@ -147,4 +151,196 @@ describe("SignupPage", () => {
|
||||
expect(pushMock).toHaveBeenCalledWith("/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
it("회원가입 페이지는 전역 라이트/다크 테마 배경 클래스를 사용해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const main = await screen.findByRole("main");
|
||||
const className = (main as HTMLElement).className;
|
||||
|
||||
expect(className).toContain("bg-slate-100");
|
||||
expect(className).toContain("dark:bg-slate-950");
|
||||
});
|
||||
|
||||
it("회원가입 폼의 비밀번호/비밀번호 확인 입력은 이메일 입력과 동일한 라이트/다크 테마 클래스를 사용해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("비밀번호 확인") as HTMLInputElement;
|
||||
|
||||
const emailClass = emailInput.className;
|
||||
const passwordClass = passwordInput.className;
|
||||
const confirmClass = confirmInput.className;
|
||||
|
||||
expect(emailClass).toContain("border-slate-300");
|
||||
expect(emailClass).toContain("bg-white");
|
||||
expect(emailClass).toContain("text-slate-900");
|
||||
expect(emailClass).toContain("dark:border-slate-700");
|
||||
expect(emailClass).toContain("dark:bg-slate-950");
|
||||
expect(emailClass).toContain("dark:text-slate-50");
|
||||
|
||||
expect(passwordClass).toBe(emailClass);
|
||||
expect(confirmClass).toBe(emailClass);
|
||||
});
|
||||
|
||||
it("회원가입 폼에는 비밀번호 확인 입력 필드가 있어야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const confirmInput = await screen.findByLabelText("비밀번호 확인");
|
||||
expect(confirmInput).toBeTruthy();
|
||||
});
|
||||
|
||||
it("비밀번호와 비밀번호 확인이 일치하지 않으면 회원가입 요청을 보내지 말고 오류 메시지를 표시해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
|
||||
const url = typeof input === "string" ? input : input.url;
|
||||
const method = init?.method ?? "GET";
|
||||
|
||||
if (url === "/api/auth/me" && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (url === "/api/auth/signup" && method === "POST") {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({ id: "1", email: "new@example.com" }), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(null, { status: 500 }));
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("비밀번호 확인") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "회원가입" });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "new@example.com" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "securePass1" } });
|
||||
fireEvent.change(confirmInput, { target: { value: "differentPass2" } });
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
const errorText = await screen.findByText(/비밀번호와 비밀번호 확인이 일치하지 않습니다/);
|
||||
expect(errorText).toBeTruthy();
|
||||
|
||||
const signupCall = fetchMock.mock.calls.find(([url, options]) => {
|
||||
return url === "/api/auth/signup" && options?.method === "POST";
|
||||
});
|
||||
|
||||
expect(signupCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it("짧고 단순한 비밀번호는 난이도가 약함으로 표시되어야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: "abc12345" } });
|
||||
|
||||
const weakText = await screen.findByText("비밀번호 난이도: 약함");
|
||||
expect(weakText).toBeTruthy();
|
||||
|
||||
const bar = await screen.findByTestId("password-strength-bar");
|
||||
const barClass = bar.className;
|
||||
expect(barClass).toContain("w-1/3");
|
||||
expect(barClass).toContain("bg-red-500");
|
||||
});
|
||||
|
||||
it("대소문자와 숫자를 섞은 비밀번호는 난이도가 보통으로 표시되어야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: "Abc12345" } });
|
||||
|
||||
const mediumText = await screen.findByText("비밀번호 난이도: 보통");
|
||||
expect(mediumText).toBeTruthy();
|
||||
|
||||
const bar = await screen.findByTestId("password-strength-bar");
|
||||
const barClass = bar.className;
|
||||
expect(barClass).toContain("w-2/3");
|
||||
expect(barClass).toContain("bg-amber-500");
|
||||
});
|
||||
|
||||
it("대소문자/숫자/특수문자가 섞인 충분히 긴 비밀번호는 난이도가 강함으로 표시되어야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: "Abc12345!@" } });
|
||||
|
||||
const strongText = await screen.findByText("비밀번호 난이도: 강함");
|
||||
expect(strongText).toBeTruthy();
|
||||
|
||||
const bar = await screen.findByTestId("password-strength-bar");
|
||||
const barClass = bar.className;
|
||||
expect(barClass).toContain("w-full");
|
||||
expect(barClass).toContain("bg-emerald-600");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { render, screen, cleanup } from "@testing-library/react";
|
||||
import { TextPropertiesPanel } from "@/app/editor/panels/TextPropertiesPanel";
|
||||
import type { TextBlockProps } from "@/features/editor/state/editorStore";
|
||||
|
||||
// TextPropertiesPanel 컨트롤 라이트/다크 테마 TDD
|
||||
|
||||
describe("TextPropertiesPanel", () => {
|
||||
const baseProps: TextBlockProps = {
|
||||
text: "텍스트",
|
||||
align: "left",
|
||||
size: "base",
|
||||
} as any;
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("텍스트 정렬 셀렉트는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<TextPropertiesPanel
|
||||
textProps={baseProps}
|
||||
selectedBlockId="text-align"
|
||||
updateBlock={updateBlock}
|
||||
editingBlockId={null}
|
||||
setEditingText={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("정렬") 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("글자 간격 프리셋 셀렉트는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<TextPropertiesPanel
|
||||
textProps={baseProps}
|
||||
selectedBlockId="text-letter-spacing"
|
||||
updateBlock={updateBlock}
|
||||
editingBlockId={null}
|
||||
setEditingText={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("글자 간격 프리셋") 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("최대 너비 프리셋 셀렉트는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<TextPropertiesPanel
|
||||
textProps={baseProps}
|
||||
selectedBlockId="text-max-width-select"
|
||||
updateBlock={updateBlock}
|
||||
editingBlockId={null}
|
||||
setEditingText={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("최대 너비 프리셋") 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("최대 너비 커스텀 인풋은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<TextPropertiesPanel
|
||||
textProps={baseProps}
|
||||
selectedBlockId="text-max-width-input"
|
||||
updateBlock={updateBlock}
|
||||
editingBlockId={null}
|
||||
setEditingText={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("최대 너비 커스텀") as HTMLInputElement;
|
||||
const className = input.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");
|
||||
});
|
||||
});
|
||||
@@ -293,4 +293,43 @@ describe("VideoPropertiesPanel", () => {
|
||||
expect.objectContaining({ captionText: "새 캡션 텍스트" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("VideoPropertiesPanel 주요 인풋/셀렉트 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
<VideoPropertiesPanel
|
||||
videoProps={{ ...baseProps, sourceType: "externalUrl" } as any}
|
||||
selectedBlockId="video-theme-1"
|
||||
updateBlock={updateBlock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const sourceSelect = screen.getByLabelText("비디오 소스") as HTMLSelectElement;
|
||||
const sourceClass = sourceSelect.getAttribute("class") ?? "";
|
||||
expect(sourceClass).toContain("bg-white");
|
||||
expect(sourceClass).toContain("text-slate-900");
|
||||
expect(sourceClass).toContain("border-slate-300");
|
||||
expect(sourceClass).toContain("dark:bg-slate-900");
|
||||
expect(sourceClass).toContain("dark:text-slate-100");
|
||||
expect(sourceClass).toContain("dark:border-slate-700");
|
||||
|
||||
const urlInput = screen.getByLabelText("비디오 URL") as HTMLInputElement;
|
||||
const urlClass = urlInput.getAttribute("class") ?? "";
|
||||
expect(urlClass).toContain("bg-white");
|
||||
expect(urlClass).toContain("text-slate-900");
|
||||
expect(urlClass).toContain("border-slate-300");
|
||||
expect(urlClass).toContain("dark:bg-slate-900");
|
||||
expect(urlClass).toContain("dark:text-slate-100");
|
||||
expect(urlClass).toContain("dark:border-slate-700");
|
||||
|
||||
const alignSelect = screen.getByLabelText("비디오 정렬") as HTMLSelectElement;
|
||||
const alignClass = alignSelect.getAttribute("class") ?? "";
|
||||
expect(alignClass).toContain("bg-white");
|
||||
expect(alignClass).toContain("text-slate-900");
|
||||
expect(alignClass).toContain("border-slate-300");
|
||||
expect(alignClass).toContain("dark:bg-slate-900");
|
||||
expect(alignClass).toContain("dark:text-slate-100");
|
||||
expect(alignClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -300,6 +300,16 @@ describe("formHelpers - public tokens", () => {
|
||||
expectNoPxInStyleObject(tokens.inputStyle, { allowKeys: ["borderRadius", "borderWidth"] });
|
||||
});
|
||||
|
||||
it("computeFormInputPublicTokens: 색상 커스텀 값이 없으면 color/backgroundColor/borderColor 를 설정하지 않아야 한다", () => {
|
||||
const tokens = computeFormInputPublicTokens({
|
||||
...baseInput,
|
||||
} as any);
|
||||
|
||||
expect(tokens.inputStyle.color).toBeUndefined();
|
||||
expect(tokens.inputStyle.backgroundColor).toBeUndefined();
|
||||
expect(tokens.inputStyle.borderColor).toBeUndefined();
|
||||
});
|
||||
|
||||
it("computeFormSelectPublicTokens: 퍼블릭 셀렉트 필드 스타일을 계산해야 한다", () => {
|
||||
const tokens = computeFormSelectPublicTokens({
|
||||
...baseSelect,
|
||||
@@ -327,6 +337,17 @@ describe("formHelpers - public tokens", () => {
|
||||
expectNoPxInStyleObject(tokens.selectStyle, { allowKeys: ["borderRadius", "borderWidth"] });
|
||||
});
|
||||
|
||||
it("computeFormSelectPublicTokens: 색상 커스텀 값이 없으면 color/backgroundColor/borderColor 를 설정하지 않아야 한다", () => {
|
||||
const tokens = computeFormSelectPublicTokens({
|
||||
...baseSelect,
|
||||
} as any);
|
||||
|
||||
expect(tokens.wrapperStyle.color).toBeUndefined();
|
||||
expect(tokens.selectStyle.color).toBeUndefined();
|
||||
expect(tokens.selectStyle.backgroundColor).toBeUndefined();
|
||||
expect(tokens.selectStyle.borderColor).toBeUndefined();
|
||||
});
|
||||
|
||||
it("computeFormCheckboxPublicTokens: 퍼블릭 체크박스 그룹 스타일을 계산해야 한다", () => {
|
||||
const tokens = computeFormCheckboxPublicTokens({
|
||||
...baseCheckbox,
|
||||
@@ -362,6 +383,17 @@ describe("formHelpers - public tokens", () => {
|
||||
expectNoPxInStyleObject(tokens.optionTextStyle);
|
||||
});
|
||||
|
||||
it("computeFormCheckboxPublicTokens: 색상 커스텀 값이 없으면 텍스트/옵션 컨테이너에 색상 스타일을 설정하지 않아야 한다", () => {
|
||||
const tokens = computeFormCheckboxPublicTokens({
|
||||
...baseCheckbox,
|
||||
} as any);
|
||||
|
||||
expect(tokens.groupTextStyle.color).toBeUndefined();
|
||||
expect(tokens.optionTextStyle.color).toBeUndefined();
|
||||
expect(tokens.optionContainerStyle.backgroundColor).toBeUndefined();
|
||||
expect(tokens.optionContainerStyle.borderColor).toBeUndefined();
|
||||
});
|
||||
|
||||
it("computeFormRadioPublicTokens: 퍼블릭 라디오 그룹 스타일을 계산해야 한다", () => {
|
||||
const tokens = computeFormRadioPublicTokens({
|
||||
...baseRadio,
|
||||
@@ -389,6 +421,17 @@ describe("formHelpers - public tokens", () => {
|
||||
expect(tokens.groupTextStyle.color).toBe("#ffffff");
|
||||
expect(tokens.optionTextStyle.color).toBe("#ffffff");
|
||||
});
|
||||
|
||||
it("computeFormRadioPublicTokens: 색상 커스텀 값이 없으면 텍스트/옵션 컨테이너에 색상 스타일을 설정하지 않아야 한다", () => {
|
||||
const tokens = computeFormRadioPublicTokens({
|
||||
...baseRadio,
|
||||
} as any);
|
||||
|
||||
expect(tokens.groupTextStyle.color).toBeUndefined();
|
||||
expect(tokens.optionTextStyle.color).toBeUndefined();
|
||||
expect(tokens.optionContainerStyle.backgroundColor).toBeUndefined();
|
||||
expect(tokens.optionContainerStyle.borderColor).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("formHelpers.computeFormControllerPublicTokens", () => {
|
||||
|
||||
Reference in New Issue
Block a user