Files
page-builder/tests/unit/PublicPageRendererVideoStyles.spec.tsx
T
jaybe c5c9d17e8b
CI / test (push) Failing after 7m30s
CI / pr_and_merge (push) Has been skipped
비디오 태그 및 폼 블록 정리
2025-11-27 19:37:32 +09:00

218 lines
7.4 KiB
TypeScript

import { describe, it, expect, afterEach, vi } from "vitest";
import { render, cleanup, fireEvent } from "@testing-library/react";
import { PublicPageRenderer } from "@/features/editor/components/PublicPageRenderer";
import type { Block } from "@/features/editor/state/editorStore";
// PublicPageRenderer 비디오 블록 스타일 TDD
// - YouTube URL 은 iframe + pb-video-wrapper 로 렌더링되어야 한다.
// - 업로드(/api/video/:id) URL 은 <video> 태그로 렌더되고 widthPx 가 em 단위로 반영되어야 한다.
describe("PublicPageRenderer - 비디오 블록 스타일", () => {
afterEach(() => {
cleanup();
});
it("YouTube URL 비디오 블록은 iframe 으로 렌더되고 기본 16:9 wrapper 를 가져야 한다", () => {
const blocks: Block[] = [
{
id: "video_youtube_preview_1",
type: "video" as any,
props: {
sourceUrl: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
align: "center",
widthMode: "auto",
aspectRatio: "16:9",
} as any,
},
];
const { container } = render(<PublicPageRenderer blocks={blocks} />);
const iframe = container.querySelector('iframe[title="비디오"]') as HTMLIFrameElement | null;
expect(iframe).not.toBeNull();
expect(iframe!.src).toContain("youtube.com/embed");
const wrapper = iframe!.parentElement as HTMLElement | null;
expect(wrapper).not.toBeNull();
expect(wrapper!.className).toContain("pb-video-wrapper");
});
it("업로드 비디오 URL 은 video 태그로 렌더되고 widthPx 가 em 단위 너비로 반영되어야 한다", () => {
const blocks: Block[] = [
{
id: "video_upload_preview_1",
type: "video" as any,
props: {
sourceUrl: "/api/video/abc123",
align: "left",
widthMode: "fixed",
widthPx: 640,
aspectRatio: "16:9",
} as any,
},
];
const { container } = render(<PublicPageRenderer blocks={blocks} />);
const video = container.querySelector("video") as HTMLVideoElement | null;
expect(video).not.toBeNull();
expect(video!.getAttribute("controls")).not.toBeNull();
// 640px / 16 = 40em
expect(video!.style.width).toBe("40em");
});
it("posterImageSrc 가 설정된 HTML5 비디오는 video 태그 poster 속성으로 렌더되어야 한다", () => {
const blocks: Block[] = [
{
id: "video_poster_preview_1",
type: "video" as any,
props: {
sourceUrl: "/videos/sample-preview.mp4",
align: "center",
widthMode: "auto",
aspectRatio: "16:9",
posterImageSrc: "/images/sample-poster-preview.png",
} as any,
},
];
const { container } = render(<PublicPageRenderer blocks={blocks} />);
const video = container.querySelector("video") as HTMLVideoElement | null;
expect(video).not.toBeNull();
expect(video!.getAttribute("poster")).toBe("/images/sample-poster-preview.png");
});
it("startTimeSec/endTimeSec 가 설정된 HTML5 비디오는 프리뷰에서도 loadedmetadata/timeupdate 로 시작/종료 범위가 적용되어야 한다", () => {
const blocks: Block[] = [
{
id: "video_range_preview_1",
type: "video" as any,
props: {
sourceUrl: "/videos/range-preview.mp4",
align: "center",
widthMode: "auto",
aspectRatio: "16:9",
startTimeSec: 3,
endTimeSec: 8,
} as any,
},
];
const { container } = render(<PublicPageRenderer blocks={blocks} />);
const video = container.querySelector("video") as HTMLVideoElement | null;
expect(video).not.toBeNull();
fireEvent(video as HTMLVideoElement, new Event("loadedmetadata"));
expect((video as HTMLVideoElement).currentTime).toBe(3);
(video as any).pause = vi.fn();
(video as HTMLVideoElement).currentTime = 8.2;
fireEvent(video as HTMLVideoElement, new Event("timeupdate"));
expect((video as any).pause).toHaveBeenCalled();
});
it("HTML5 비디오의 ariaLabel/titleText/captionText 는 프리뷰에서 aria-label 및 캡션 요소로 반영되어야 한다", () => {
const blocks: Block[] = [
{
id: "video_a11y_preview",
type: "video" as any,
props: {
sourceUrl: "/videos/a11y-preview.mp4",
align: "center",
widthMode: "auto",
aspectRatio: "16:9",
titleText: "프리뷰 접근성 타이틀",
ariaLabel: "프리뷰 접근성 설명",
captionText: "프리뷰 비디오 캡션입니다.",
} as any,
},
];
const { container, getByTestId } = render(<PublicPageRenderer blocks={blocks} />);
const video = container.querySelector("video") as HTMLVideoElement | null;
expect(video).not.toBeNull();
expect(video!.getAttribute("aria-label")).toBe("프리뷰 접근성 설명");
const caption = getByTestId("preview-video-caption");
expect(caption.textContent).toBe("프리뷰 비디오 캡션입니다.");
});
it("titleText 가 설정된 YouTube 비디오는 iframe title 로 반영되어야 한다", () => {
const blocks: Block[] = [
{
id: "video_youtube_a11y_1",
type: "video" as any,
props: {
sourceUrl: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
align: "center",
widthMode: "auto",
aspectRatio: "16:9",
titleText: "사용자 정의 비디오 제목",
} as any,
},
];
const { container } = render(<PublicPageRenderer blocks={blocks} />);
const iframe = container.querySelector('iframe[title="사용자 정의 비디오 제목"]') as HTMLIFrameElement | null;
expect(iframe).not.toBeNull();
});
it("비디오 카드 스타일 props(backgroundColorCustom/cardPaddingPx/borderRadiusPx)는 wrapper 의 스타일에 반영되어야 한다", () => {
const blocks: Block[] = [
{
id: "video_card_preview_1",
type: "video" as any,
props: {
sourceUrl: "/api/video/preview-card",
align: "center",
widthMode: "fixed",
widthPx: 320,
aspectRatio: "16:9",
backgroundColorCustom: "#123456",
cardPaddingPx: 32,
borderRadiusPx: 16,
} as any,
},
];
const { container } = render(<PublicPageRenderer blocks={blocks} />);
const video = container.querySelector("video") as HTMLVideoElement | null;
expect(video).not.toBeNull();
const wrapper = video!.parentElement as HTMLElement | null;
expect(wrapper).not.toBeNull();
// #123456 -> rgb(18, 52, 86)
expect(wrapper!.style.backgroundColor).toBe("rgb(18, 52, 86)");
// 32px / 16 = 2em, 16px / 16 = 1em
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();
});
});