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 = {}): 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( , ); const input = screen.getByLabelText("성공 메시지"); 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( , ); const input = screen.getByLabelText("에러 메시지"); 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( , ); 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( , ); const select = screen.getByLabelText("Submit 버튼") 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( , ); const guideButton = screen.getByRole("button", { name: "Google Sheets 연동 가이드" }); guideButton.click(); // Google Sheets 연동 가이드 헤딩과 Apps Script 안내 문구가 노출되어야 한다. expect(screen.getByText("Google Sheets 연동 가이드")).toBeTruthy(); expect( screen.getByText(/Apps Script 웹 앱 URL/i), ).toBeTruthy(); }); it("Google Sheets 가이드 모달의 Apps Script 코드가 컨트롤러에 매핑된 전송 키와 작성일시를 포함해야 한다", 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( , ); const guideButton = screen.getByRole("button", { name: "Google Sheets 연동 가이드" }); 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( , ); 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( , ); 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( , ); 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( , ); 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"); }); });