import { describe, it, expect, vi, afterEach } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import { VideoPropertiesPanel } from "@/app/editor/panels/VideoPropertiesPanel"; import type { VideoBlockProps } from "@/features/editor/state/editorStore"; // VideoPropertiesPanel 컨트롤 TDD // - 비디오 URL 인풋/정렬/너비 모드/화면 비율 컨트롤이 updateBlock 을 올바르게 호출하는지 검증한다. describe("VideoPropertiesPanel", () => { const baseProps: VideoBlockProps = { sourceUrl: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", align: "center", widthMode: "auto", aspectRatio: "16:9", }; afterEach(() => { cleanup(); }); it("비디오 URL 인풋 변경 시 updateBlock 이 sourceUrl / sourceType / assetId 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const urlInput = screen.getByLabelText("Video URL"); fireEvent.change(urlInput, { target: { value: "https://example.com/video.mp4" } }); expect(updateBlock).toHaveBeenCalledWith( "video-1", expect.objectContaining({ sourceUrl: "https://example.com/video.mp4", sourceType: "externalUrl", assetId: null, }), ); }); it("정렬 셀렉트 변경 시 updateBlock 이 align 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("Video alignment"); fireEvent.change(select, { target: { value: "left" } }); expect(updateBlock).toHaveBeenCalledWith( "video-2", expect.objectContaining({ align: "left" }), ); }); it("너비 모드 셀렉트 변경 시 updateBlock 이 widthMode 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("Video width mode"); fireEvent.change(select, { target: { value: "fixed" } }); expect(updateBlock).toHaveBeenCalledWith( "video-3", expect.objectContaining({ widthMode: "fixed" }), ); }); it("화면 비율 셀렉트 변경 시 updateBlock 이 aspectRatio 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const select = screen.getByLabelText("Video aspect ratio"); fireEvent.change(select, { target: { value: "4:3" } }); expect(updateBlock).toHaveBeenCalledWith( "video-4", expect.objectContaining({ aspectRatio: "4:3" }), ); }); it("카드 배경색 HEX 인풋 변경 시 updateBlock 이 backgroundColorCustom 으로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const hexInput = screen.getByLabelText("Video card background color HEX"); fireEvent.change(hexInput, { target: { value: "#123456" } }); expect(updateBlock).toHaveBeenCalledWith( "video-card-1", expect.objectContaining({ backgroundColorCustom: "#123456" }), ); }); it("카드 패딩 슬라이더 변경 시 updateBlock 이 cardPaddingPx 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("Card padding 슬라이더"); fireEvent.change(slider, { target: { value: "24" } }); expect(updateBlock).toHaveBeenCalledWith( "video-card-2", expect.objectContaining({ cardPaddingPx: 24 }), ); }); it("카드 모서리 둥글기 슬라이더 변경 시 updateBlock 이 borderRadiusPx 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("Card border radius 슬라이더"); fireEvent.change(slider, { target: { value: "20" } }); expect(updateBlock).toHaveBeenCalledWith( "video-card-3", expect.objectContaining({ borderRadiusPx: 20 }), ); }); it("포스터 이미지 URL 인풋 변경 시 updateBlock 이 posterImageSrc / posterSourceType / posterAssetId 로 호출되어야 한다", () => { const updateBlock = vi.fn(); const posterProps: VideoBlockProps = { ...baseProps, sourceType: "externalUrl", posterImageSrc: "https://example.com/poster-before.png", posterSourceType: "externalUrl", posterAssetId: null, }; render( , ); const input = screen.getByLabelText("Poster image URL"); fireEvent.change(input, { target: { value: "https://example.com/poster-after.png" } }); expect(updateBlock).toHaveBeenCalledWith( "video-poster-1", expect.objectContaining({ posterImageSrc: "https://example.com/poster-after.png", posterSourceType: "externalUrl", posterAssetId: null, }), ); }); it("시작 시점 슬라이더 변경 시 updateBlock 이 startTimeSec 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("Start time (sec) 슬라이더"); fireEvent.change(slider, { target: { value: "5" } }); expect(updateBlock).toHaveBeenCalledWith( "video-range-1", expect.objectContaining({ startTimeSec: 5 }), ); }); it("종료 시점 슬라이더 변경 시 updateBlock 이 endTimeSec 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const slider = screen.getByLabelText("End time (sec) 슬라이더"); fireEvent.change(slider, { target: { value: "15" } }); expect(updateBlock).toHaveBeenCalledWith( "video-range-2", expect.objectContaining({ endTimeSec: 15 }), ); }); it("비디오 제목 인풋 변경 시 updateBlock 이 titleText 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const input = screen.getByLabelText("Video title"); fireEvent.change(input, { target: { value: "새 비디오 제목" } }); expect(updateBlock).toHaveBeenCalledWith( "video-a11y-1", expect.objectContaining({ titleText: "새 비디오 제목" }), ); }); it("비디오 aria-label 인풋 변경 시 updateBlock 이 ariaLabel 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const input = screen.getByLabelText("Video aria-label"); fireEvent.change(input, { target: { value: "새 aria-label" } }); expect(updateBlock).toHaveBeenCalledWith( "video-a11y-2", expect.objectContaining({ ariaLabel: "새 aria-label" }), ); }); it("비디오 캡션 텍스트 인풋 변경 시 updateBlock 이 captionText 로 호출되어야 한다", () => { const updateBlock = vi.fn(); render( , ); const input = screen.getByLabelText("Video caption text"); fireEvent.change(input, { target: { value: "새 캡션 텍스트" } }); expect(updateBlock).toHaveBeenCalledWith( "video-a11y-3", expect.objectContaining({ captionText: "새 캡션 텍스트" }), ); }); it("VideoPropertiesPanel 주요 인풋/셀렉트 컨트롤은 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => { const updateBlock = vi.fn(); render( , ); const sourceSelect = screen.getByLabelText("Video source") 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("Video 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("Video alignment") 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"); }); });