403 lines
13 KiB
TypeScript
403 lines
13 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
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";
|
|
|
|
// FormControllerPanel 컨트롤 TDD
|
|
// - 성공/에러 메시지 인풋이 updateBlock 을 통해 FormBlockProps.successMessage / errorMessage 를 갱신하는지 검증한다.
|
|
|
|
function makeFormBlock(id: string, props: Partial<FormBlockProps> = {}): Block {
|
|
const base: FormBlockProps = {
|
|
kind: "contact",
|
|
submitTarget: "internal",
|
|
fieldIds: [],
|
|
submitButtonId: null,
|
|
} as any;
|
|
|
|
return {
|
|
id,
|
|
type: "form",
|
|
props: { ...base, ...props } as any,
|
|
} as any;
|
|
}
|
|
|
|
describe("FormControllerPanel", () => {
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
it("성공 메시지 인풋 변경 시 updateBlock 이 successMessage 로 호출되어야 한다", () => {
|
|
const formBlock = makeFormBlock("form-1", {
|
|
successMessage: "기존 성공 메시지",
|
|
});
|
|
|
|
const updateBlock = vi.fn();
|
|
|
|
render(
|
|
<FormControllerPanel
|
|
block={formBlock}
|
|
blocks={[formBlock]}
|
|
selectedBlockId="form-1"
|
|
updateBlock={updateBlock}
|
|
/>,
|
|
);
|
|
|
|
const input = screen.getByLabelText("Success message");
|
|
fireEvent.change(input, { target: { value: "새 성공 메시지" } });
|
|
|
|
expect(updateBlock).toHaveBeenCalledWith(
|
|
"form-1",
|
|
expect.objectContaining({ successMessage: "새 성공 메시지" }),
|
|
);
|
|
});
|
|
|
|
it("에러 메시지 인풋 변경 시 updateBlock 이 errorMessage 로 호출되어야 한다", () => {
|
|
const formBlock = makeFormBlock("form-2", {
|
|
errorMessage: "기존 에러 메시지",
|
|
});
|
|
|
|
const updateBlock = vi.fn();
|
|
|
|
render(
|
|
<FormControllerPanel
|
|
block={formBlock}
|
|
blocks={[formBlock]}
|
|
selectedBlockId="form-2"
|
|
updateBlock={updateBlock}
|
|
/>,
|
|
);
|
|
|
|
const input = screen.getByLabelText("Error message");
|
|
fireEvent.change(input, { target: { value: "새 에러 메시지" } });
|
|
|
|
expect(updateBlock).toHaveBeenCalledWith(
|
|
"form-2",
|
|
expect.objectContaining({ errorMessage: "새 에러 메시지" }),
|
|
);
|
|
});
|
|
|
|
it("폼 필드 매핑 UI 에서 필드 라벨 대신 전송 키(formFieldName)를 우선적으로 표시해야 한다", () => {
|
|
const formBlock = makeFormBlock("form-controller", {
|
|
fieldIds: ["input-1", "checkbox-1"],
|
|
});
|
|
|
|
const inputBlock: Block = {
|
|
id: "input-1",
|
|
type: "formInput",
|
|
props: {
|
|
label: "이름",
|
|
formFieldName: "name",
|
|
} as any,
|
|
} as any;
|
|
|
|
const checkboxBlock: Block = {
|
|
id: "checkbox-1",
|
|
type: "formCheckbox",
|
|
props: {
|
|
groupLabel: "옵션",
|
|
formFieldName: "options",
|
|
options: [],
|
|
} as any,
|
|
} as any;
|
|
|
|
const updateBlock = vi.fn();
|
|
|
|
render(
|
|
<FormControllerPanel
|
|
block={formBlock}
|
|
blocks={[formBlock, inputBlock, checkboxBlock]}
|
|
selectedBlockId="form-controller"
|
|
updateBlock={updateBlock}
|
|
/>,
|
|
);
|
|
|
|
const nameLabel = screen.getByText("name (이름)");
|
|
const optionsLabel = screen.getByText("options (옵션)");
|
|
|
|
expect(nameLabel).toBeTruthy();
|
|
expect(optionsLabel).toBeTruthy();
|
|
});
|
|
|
|
it("Submit 버튼 셀렉트에서 버튼 블록의 전송 키(formFieldName)를 기준으로 표시해야 한다", () => {
|
|
const formBlock = makeFormBlock("form-controller", {
|
|
submitButtonId: "btn-1",
|
|
});
|
|
|
|
const buttonBlock: Block = {
|
|
id: "btn-1",
|
|
type: "button",
|
|
props: {
|
|
label: "제출하기",
|
|
formFieldName: "submit-main",
|
|
} as any,
|
|
} as any;
|
|
|
|
const updateBlock = vi.fn();
|
|
|
|
render(
|
|
<FormControllerPanel
|
|
block={formBlock}
|
|
blocks={[formBlock, buttonBlock]}
|
|
selectedBlockId="form-controller"
|
|
updateBlock={updateBlock}
|
|
/>,
|
|
);
|
|
|
|
const select = screen.getByLabelText("Submit button") as HTMLSelectElement;
|
|
|
|
const submitMainOption = screen.getByText("submit-main (제출하기)");
|
|
|
|
expect(submitMainOption).toBeTruthy();
|
|
// 기본 선택 값은 FormBlock.submitButtonId 이어야 한다.
|
|
expect(select.value).toBe("btn-1");
|
|
});
|
|
|
|
it("submitTarget 이 webhook 인 경우 Google Sheets 연동 가이드 텍스트를 보여줘야 한다", () => {
|
|
const formBlock = makeFormBlock("form-webhook", {
|
|
submitTarget: "webhook",
|
|
} as any);
|
|
|
|
const updateBlock = vi.fn();
|
|
|
|
render(
|
|
<FormControllerPanel
|
|
block={formBlock}
|
|
blocks={[formBlock]}
|
|
selectedBlockId="form-webhook"
|
|
updateBlock={updateBlock}
|
|
/>,
|
|
);
|
|
|
|
const guideButton = screen.getByRole("button", { name: "Google Sheets integration guide" });
|
|
guideButton.click();
|
|
|
|
// Google Sheets integration guide heading and Apps Script web app URL hint should be visible.
|
|
expect(screen.getByText("Google Sheets integration guide")).toBeTruthy();
|
|
expect(screen.getByText(/Apps Script web app URL/i)).toBeTruthy();
|
|
});
|
|
|
|
it("Google Sheets guide modal Apps Script code should include mapped submit keys and createdAt field", async () => {
|
|
const formBlock = makeFormBlock("form-webhook", {
|
|
submitTarget: "webhook",
|
|
fieldIds: ["input-name", "input-email"],
|
|
} as any);
|
|
|
|
const inputNameBlock: Block = {
|
|
id: "input-name",
|
|
type: "formInput",
|
|
props: {
|
|
label: "이름",
|
|
formFieldName: "name",
|
|
} as any,
|
|
} as any;
|
|
|
|
const inputEmailBlock: Block = {
|
|
id: "input-email",
|
|
type: "formInput",
|
|
props: {
|
|
label: "이메일",
|
|
formFieldName: "email",
|
|
} as any,
|
|
} as any;
|
|
|
|
const updateBlock = vi.fn();
|
|
|
|
render(
|
|
<FormControllerPanel
|
|
block={formBlock}
|
|
blocks={[formBlock, inputNameBlock, inputEmailBlock]}
|
|
selectedBlockId="form-webhook"
|
|
updateBlock={updateBlock}
|
|
/>,
|
|
);
|
|
|
|
const guideButton = screen.getByRole("button", { name: "Google Sheets integration guide" });
|
|
fireEvent.click(guideButton);
|
|
|
|
const scriptTextarea = (await screen.findByDisplayValue(/function doPost/)) as HTMLTextAreaElement;
|
|
|
|
expect(scriptTextarea.value).toContain("params.name || \"\"");
|
|
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("Submit target") 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("Form width mode") 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("Success message") 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("Error message") 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(/Additional parameters/) 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 integration guide modal card and textarea should use dual light/dark theme chrome", () => {
|
|
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 integration guide" });
|
|
fireEvent.click(guideButton);
|
|
|
|
const heading = screen.getByRole("heading", { name: "Google Sheets integration guide" });
|
|
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: "Form field mapping" });
|
|
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 button") 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");
|
|
});
|
|
});
|