비디오 태그 및 폼 블록 정리
CI / test (push) Failing after 7m30s
CI / pr_and_merge (push) Has been skipped

This commit is contained in:
2025-11-27 19:37:32 +09:00
parent 1405280ed3
commit c5c9d17e8b
50 changed files with 365 additions and 173 deletions
+91
View File
@@ -0,0 +1,91 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { render, screen, fireEvent, cleanup, waitFor } from "@testing-library/react";
import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
import PreviewPage from "@/app/preview/page";
// PreviewPage Export ZIP TDD
// - 프리뷰 헤더에 "페이지 파일로 내보내기 (ZIP)" 버튼이 노출되어야 한다.
// - 버튼 클릭 시 /api/export 로 blocks + projectConfig 를 payload 로 POST 요청을 보내야 한다.
let mockState: any;
beforeEach(() => {
const baseProjectConfig: ProjectConfig = {
title: "프리뷰 Export 테스트",
slug: "preview-export-test",
canvasPreset: "full",
} as ProjectConfig;
mockState = {
blocks: [
{
id: "blk_text_1",
type: "text",
props: {
text: "프리뷰 Export 본문",
align: "left",
size: "base",
},
},
] as Block[],
projectConfig: baseProjectConfig,
};
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
vi.mock("@/features/editor/state/editorStore", () => {
const useEditorStore = (selector: (state: any) => any) => selector(mockState);
return {
__esModule: true,
useEditorStore,
};
});
vi.mock("next/link", () => {
return {
__esModule: true,
default: ({ href, children }: any) => <a href={href}>{children}</a>,
};
});
describe("PreviewPage - ZIP Export", () => {
it("프리뷰 헤더에 ZIP Export 버튼이 노출되어야 한다", () => {
render(<PreviewPage />);
const exportButton = screen.getByText("페이지 파일로 내보내기 (ZIP)");
expect(exportButton).toBeTruthy();
});
it("ZIP Export 버튼 클릭 시 /api/export 로 blocks + projectConfig 를 payload 로 POST 해야 한다", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
// JSDOM 환경에서 blob() 호출을 안전하게 처리하기 위한 최소 mock
blob: vi.fn().mockResolvedValue(new Blob(["dummy-zip"])),
} as any);
vi.stubGlobal("fetch", fetchMock);
render(<PreviewPage />);
const exportButton = screen.getByText("페이지 파일로 내보내기 (ZIP)");
fireEvent.click(exportButton);
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});
const [url, options] = fetchMock.mock.calls[0] as any;
expect(url).toBe("/api/export");
expect(options.method).toBe("POST");
expect(options.headers["Content-Type"]).toBe("application/json");
const parsed = JSON.parse(options.body);
expect(Array.isArray(parsed.blocks)).toBe(true);
expect(parsed.blocks[0].id).toBe("blk_text_1");
expect(parsed.projectConfig.slug).toBe("preview-export-test");
});
});
@@ -88,7 +88,7 @@ describe("PublicPageRenderer - 블록 배경색", () => {
expect(listNoBg!.style.backgroundColor === "" || listNoBg!.style.backgroundColor === "transparent").toBe(true);
});
it("form 컨트롤러 블록은 backgroundColorCustom 이 설정된 경우에만 폼 래퍼에 배경색이 적용되어야 한다", () => {
it("form 컨트롤러 블록은 프리뷰에서 별도 폼 래퍼를 렌더하지 않아야 한다", () => {
const blocksWithBg: Block[] = [
{
id: "form_with_bg",
@@ -105,10 +105,10 @@ describe("PublicPageRenderer - 블록 배경색", () => {
} as any,
];
const { getByTestId, rerender } = render(<PublicPageRenderer blocks={blocksWithBg} />);
const { queryByTestId, rerender } = render(<PublicPageRenderer blocks={blocksWithBg} />);
const formWithBg = getByTestId("preview-form-controller") as HTMLFormElement;
expect(formWithBg.style.backgroundColor).toBe("rgb(17, 17, 17)");
const formWithBg = queryByTestId("preview-form-controller");
expect(formWithBg).toBeNull();
const blocksWithoutBg: Block[] = [
{
@@ -126,8 +126,8 @@ describe("PublicPageRenderer - 블록 배경색", () => {
rerender(<PublicPageRenderer blocks={blocksWithoutBg} />);
const formNoBg = getByTestId("preview-form-controller") as HTMLFormElement;
expect(formNoBg.style.backgroundColor === "" || formNoBg.style.backgroundColor === "transparent").toBe(true);
const formNoBg = queryByTestId("preview-form-controller");
expect(formNoBg).toBeNull();
});
it("section 블록은 backgroundColorCustom 이 설정된 경우에만 섹션 요소에 배경색이 적용되어야 한다", () => {
@@ -15,7 +15,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
(global as any).fetch = undefined;
});
it("성공 응답 시 config.successMessage 를 success 스타일로 표시해야 한다", async () => {
it("FormBlock 이 있어도 프리뷰에서는 폼 컨트롤러를 렌더하지 않아야 한다 (성공 메시지 설정)", () => {
const blocks: Block[] = [
{
id: "form_success",
@@ -42,19 +42,12 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
render(<PublicPageRenderer blocks={blocks} />);
const form = screen.getByTestId("preview-form-controller");
fireEvent.submit(form);
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});
const messageEl = await screen.findByText("폼 성공 메시지 (config)");
expect(messageEl.textContent).toBe("폼 성공 메시지 (config)");
expect(messageEl.className).toContain("text-emerald-400");
const form = screen.queryByTestId("preview-form-controller");
expect(form).toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
});
it("에러 응답 시 config.errorMessage 를 error 스타일로 표시해야 한다", async () => {
it("FormBlock 이 있어도 프리뷰에서는 폼 컨트롤러나 에러 메시지를 렌더하지 않아야 한다", () => {
const blocks: Block[] = [
{
id: "form_error",
@@ -81,15 +74,9 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
render(<PublicPageRenderer blocks={blocks} />);
const form = screen.getByTestId("preview-form-controller");
fireEvent.submit(form);
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});
const messageEl = await screen.findByText("폼 에러 메시지 (config)");
expect(messageEl.textContent).toBe("폼 에러 메시지 (config)");
expect(messageEl.className).toContain("text-rose-400");
const form = screen.queryByTestId("preview-form-controller");
expect(form).toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
expect(screen.queryByText("폼 에러 메시지 (config)")).toBeNull();
});
});
@@ -192,4 +192,26 @@ describe("PublicPageRenderer - 비디오 블록 스타일", () => {
expect(wrapper!.style.padding).toBe("2em");
expect(wrapper!.style.borderRadius).toBe("1em");
});
it("sourceUrl 이 비어 있는 비디오 블록은 프리뷰에서 video/iframe 을 렌더하지 않아야 한다", () => {
const blocks: Block[] = [
{
id: "video_empty_src_preview_1",
type: "video" as any,
props: {
sourceUrl: "",
align: "center",
widthMode: "auto",
aspectRatio: "16:9",
} as any,
},
];
const { container } = render(<PublicPageRenderer blocks={blocks} />);
const video = container.querySelector("video");
const iframe = container.querySelector("iframe");
expect(video).toBeNull();
expect(iframe).toBeNull();
});
});