video 블록 및 리펙터링
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user