video 블록 및 리펙터링
This commit is contained in:
+730
-2
@@ -73,6 +73,144 @@ describe("/api/export", () => {
|
||||
expect(html).toContain('style="background-color:#111111;margin:0;padding:0;"');
|
||||
});
|
||||
|
||||
it("업로드 디렉터리에 이미지 파일이 없어도 ZIP 생성은 실패하지 않아야 한다", async () => {
|
||||
const missingImageId = `missing-image-${Date.now()}`;
|
||||
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_image_missing",
|
||||
type: "image",
|
||||
props: {
|
||||
src: `/api/image/${missingImageId}`,
|
||||
alt: "누락된 이미지",
|
||||
align: "center",
|
||||
widthMode: "auto",
|
||||
borderRadius: "md",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "이미지 파일 누락 내보내기 테스트",
|
||||
slug: "image-missing-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
// 업로드 파일이 없으므로 images/ 디렉터리에 엔트리는 없어야 한다.
|
||||
const imageEntry = zip.file(`images/${missingImageId}`);
|
||||
expect(imageEntry).toBeNull();
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
// HTML 은 원본 /api/image 경로를 그대로 유지하거나, 최소한 오류 없이 렌더되어야 한다.
|
||||
expect(html).toContain(`/api/image/${missingImageId}`);
|
||||
});
|
||||
|
||||
it("업로드 디렉터리에 비디오 파일이 없어도 ZIP 생성은 실패하지 않아야 한다", async () => {
|
||||
const missingVideoId = `missing-video-${Date.now()}`;
|
||||
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_video_missing",
|
||||
type: "video",
|
||||
props: {
|
||||
sourceUrl: `/api/video/${missingVideoId}`,
|
||||
align: "center",
|
||||
widthMode: "auto",
|
||||
aspectRatio: "16:9",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "비디오 파일 누락 내보내기 테스트",
|
||||
slug: "video-missing-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const videoEntry = zip.file(`videos/${missingVideoId}`);
|
||||
expect(videoEntry).toBeNull();
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
// HTML 은 원본 /api/video 경로를 그대로 유지하거나, 최소한 오류 없이 렌더되어야 한다.
|
||||
expect(html).toContain(`/api/video/${missingVideoId}`);
|
||||
});
|
||||
|
||||
it("video 블록의 sourceUrl 이 비어 있어도 ZIP 생성은 실패하지 않아야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_video_empty_source",
|
||||
type: "video",
|
||||
props: {
|
||||
sourceUrl: "",
|
||||
align: "center",
|
||||
widthMode: "auto",
|
||||
aspectRatio: "16:9",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "비디오 sourceUrl 빈 값 내보내기 테스트",
|
||||
slug: "video-empty-source-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
// sourceUrl 이 비어 있어도 index.html 은 정상 생성되어야 한다.
|
||||
expect(html).toContain("<video");
|
||||
});
|
||||
|
||||
it("canvasPreset / canvasWidthPx / 배경색은 정적 HTML 캔버스 wrapper 및 body 스타일에 반영되어야 한다", () => {
|
||||
const blocks: Block[] = [];
|
||||
|
||||
@@ -259,6 +397,55 @@ describe("/api/export", () => {
|
||||
expect(html).not.toMatch(/name=\"plan\"[^>]*required/);
|
||||
});
|
||||
|
||||
|
||||
it("FormBlock 에 fieldIds/fields 가 모두 없으면 기본 폼/필드를 내보내지 않아야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_fallback_1",
|
||||
type: "form",
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
// fieldIds, fields 를 모두 생략해 fallback 경로를 타게 한다.
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "폼 fallback 내보내기 테스트",
|
||||
slug: "form-fallback-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(arrayBuffer);
|
||||
|
||||
const indexEntry = zip.file("index.html");
|
||||
expect(indexEntry).toBeTruthy();
|
||||
|
||||
const html = await indexEntry!.async("string");
|
||||
|
||||
// 빈 FormBlock 은 컨트롤러 역할만 하고, 정적 HTML 에서는 기본 폼/필드를 생성하지 않아야 한다.
|
||||
expect(html).not.toContain("<form");
|
||||
expect(html).not.toMatch(/name=\"name\"/);
|
||||
expect(html).not.toMatch(/name=\"email\"/);
|
||||
expect(html).not.toMatch(/name=\"message\"/);
|
||||
});
|
||||
|
||||
it("/api/image/:id 기반 이미지 URL은 ZIP 안 images/ 디렉터리로 복사되고 HTML에서 상대 경로로 치환되어야 한다", async () => {
|
||||
const imageId = `test-image-${Date.now()}`;
|
||||
const uploadDir = path.join(process.cwd(), "uploads");
|
||||
@@ -312,6 +499,460 @@ describe("/api/export", () => {
|
||||
expect(html).not.toContain(`/api/image/${imageId}`);
|
||||
});
|
||||
|
||||
it("비디오 포스터 이미지 /api/image/:id 는 ZIP 안 images/ 로 복사되고 poster 경로가 상대 경로로 치환되어야 한다", async () => {
|
||||
const posterId = `test-video-poster-${Date.now()}`;
|
||||
const uploadDir = path.join(process.cwd(), "uploads");
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
await fs.writeFile(path.join(uploadDir, posterId), Buffer.from("dummy-poster"));
|
||||
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_video_poster",
|
||||
type: "video" as any,
|
||||
props: {
|
||||
sourceUrl: "https://example.com/video.mp4",
|
||||
align: "center",
|
||||
widthMode: "auto",
|
||||
aspectRatio: "16:9",
|
||||
posterImageSrc: `/api/image/${posterId}`,
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "비디오 포스터 내보내기 테스트",
|
||||
slug: "video-poster-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const posterEntry = zip.file(`images/${posterId}`);
|
||||
expect(posterEntry).toBeTruthy();
|
||||
|
||||
const posterBytes = await posterEntry!.async("nodebuffer");
|
||||
expect(posterBytes.length).toBeGreaterThan(0);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
expect(html).toContain(`poster=\"./images/${posterId}\"`);
|
||||
expect(html).not.toContain(`/api/image/${posterId}`);
|
||||
});
|
||||
|
||||
it("HTML5 비디오 startTimeSec/endTimeSec 는 정적 HTML video 태그 data-start-seconds/data-end-seconds 속성으로 반영되어야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_video_range",
|
||||
type: "video" as any,
|
||||
props: {
|
||||
sourceUrl: "https://example.com/video-range.mp4",
|
||||
align: "center",
|
||||
widthMode: "auto",
|
||||
aspectRatio: "16:9",
|
||||
startTimeSec: 5,
|
||||
endTimeSec: 12,
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "비디오 시작/종료 시점 내보내기 테스트",
|
||||
slug: "video-range-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
expect(html).toContain("data-start-seconds=\"5\"");
|
||||
expect(html).toContain("data-end-seconds=\"12\"");
|
||||
});
|
||||
|
||||
it("섹션 backgroundImageSrc 에 /api/image/:id 가 포함된 경우에도 ZIP images/ 및 상대 경로로 치환되어야 한다", async () => {
|
||||
const imageId = `test-section-bg-${Date.now()}`;
|
||||
const uploadDir = path.join(process.cwd(), "uploads");
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
await fs.writeFile(path.join(uploadDir, imageId), Buffer.from("dummy-image"));
|
||||
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "sec_bg",
|
||||
type: "section",
|
||||
props: {
|
||||
background: "default",
|
||||
paddingY: "md",
|
||||
columns: [{ id: "sec_bg_col_1", span: 12 }],
|
||||
backgroundImageSrc: `/api/image/${imageId}`,
|
||||
backgroundImageSize: "cover",
|
||||
backgroundImagePosition: "center",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "sec_bg_text",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "섹션 배경 이미지 내보내기 테스트",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
sectionId: "sec_bg",
|
||||
columnId: "sec_bg_col_1",
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "섹션 배경 이미지 내보내기 테스트",
|
||||
slug: "section-bg-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const imageEntry = zip.file(`images/${imageId}`);
|
||||
expect(imageEntry).toBeTruthy();
|
||||
|
||||
const imageBytes = await imageEntry!.async("nodebuffer");
|
||||
expect(imageBytes.length).toBeGreaterThan(0);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
expect(html).toContain(`background-image:url(./images/${imageId})`);
|
||||
expect(html).not.toContain(`/api/image/${imageId}`);
|
||||
});
|
||||
|
||||
it("섹션 backgroundImageRepeat 가 설정되면 정적 HTML style 에 background-repeat 이 포함되어야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "sec_bg_repeat",
|
||||
type: "section",
|
||||
props: {
|
||||
background: "default",
|
||||
paddingY: "md",
|
||||
columns: [{ id: "sec_bg_repeat_col_1", span: 12 }],
|
||||
backgroundImageSrc: "/images/pattern.png",
|
||||
backgroundImageSize: "auto",
|
||||
backgroundImageRepeat: "repeat-x",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "섹션 배경 반복 테스트",
|
||||
slug: "section-bg-repeat-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
expect(html).toContain("background-repeat:repeat-x");
|
||||
});
|
||||
|
||||
it("섹션 backgroundImagePositionMode 가 custom 이고 X/Y 퍼센트가 설정되면 정적 HTML background-position 에 'X% Y%' 가 포함되어야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "sec_bg_xy",
|
||||
type: "section",
|
||||
props: {
|
||||
background: "default",
|
||||
paddingY: "md",
|
||||
columns: [{ id: "sec_bg_xy_col_1", span: 12 }],
|
||||
backgroundImageSrc: "/images/section-bg-xy.png",
|
||||
backgroundImageSize: "cover",
|
||||
backgroundImagePositionMode: "custom",
|
||||
backgroundImagePositionXPercent: 10,
|
||||
backgroundImagePositionYPercent: 90,
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "섹션 배경 XY 위치 테스트",
|
||||
slug: "section-bg-xy-position-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
expect(html).toContain("background-position:10% 90%");
|
||||
});
|
||||
|
||||
it("HTML5 비디오 ariaLabel/captionText 는 정적 HTML video 태그 aria-label 및 pb-video-caption 으로 반영되어야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_video_a11y_html5",
|
||||
type: "video" as any,
|
||||
props: {
|
||||
sourceUrl: "https://example.com/video-a11y.mp4",
|
||||
align: "center",
|
||||
widthMode: "auto",
|
||||
aspectRatio: "16:9",
|
||||
ariaLabel: "정적 내보내기 접근성 비디오 설명",
|
||||
captionText: "정적 내보내기 비디오 캡션입니다.",
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "비디오 접근성 HTML5 내보내기 테스트",
|
||||
slug: "video-a11y-html5-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
expect(html).toContain('aria-label="정적 내보내기 접근성 비디오 설명"');
|
||||
expect(html).toContain('<p class="pb-video-caption">정적 내보내기 비디오 캡션입니다.</p>');
|
||||
});
|
||||
|
||||
it("titleText 가 설정된 YouTube 비디오는 정적 HTML iframe title 로 반영되어야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_video_a11y_youtube",
|
||||
type: "video" as any,
|
||||
props: {
|
||||
sourceUrl: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
align: "center",
|
||||
widthMode: "auto",
|
||||
aspectRatio: "16:9",
|
||||
titleText: "정적 내보내기 YouTube 비디오 제목",
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "비디오 접근성 YouTube 내보내기 테스트",
|
||||
slug: "video-a11y-youtube-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
expect(html).toContain('iframe title="정적 내보내기 YouTube 비디오 제목"');
|
||||
});
|
||||
|
||||
it("/api/video/:id 기반 비디오 URL은 ZIP 안 videos/ 디렉터리로 복사되고 HTML에서 상대 경로로 치환되어야 한다", async () => {
|
||||
const videoId = `test-video-${Date.now()}`;
|
||||
const uploadDir = path.join(process.cwd(), "uploads");
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
await fs.writeFile(path.join(uploadDir, videoId), Buffer.from("dummy-video"));
|
||||
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "blk_video",
|
||||
type: "video" as any,
|
||||
props: {
|
||||
sourceUrl: `/api/video/${videoId}`,
|
||||
align: "center",
|
||||
widthMode: "auto",
|
||||
aspectRatio: "16:9",
|
||||
backgroundColorCustom: "#123456",
|
||||
cardPaddingPx: 24,
|
||||
borderRadiusPx: 16,
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "비디오 내보내기 테스트",
|
||||
slug: "video-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const videoEntry = zip.file(`videos/${videoId}`);
|
||||
expect(videoEntry).toBeTruthy();
|
||||
|
||||
const videoBytes = await videoEntry!.async("nodebuffer");
|
||||
expect(videoBytes.length).toBeGreaterThan(0);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
expect(html).toContain(`src="./videos/${videoId}`);
|
||||
expect(html).not.toContain(`/api/video/${videoId}`);
|
||||
// 비디오 카드 스타일이 wrapper style 에 반영되어야 한다.
|
||||
expect(html).toContain("background-color:#123456");
|
||||
expect(html).toContain("padding:24px");
|
||||
expect(html).toContain("border-radius:16px");
|
||||
});
|
||||
|
||||
it("섹션 backgroundVideoSrc 가 /api/video/:id 인 경우 ZIP videos/ 및 상대 경로로 치환되고 섹션 안에 배경 비디오 video 태그로 렌더되어야 한다", async () => {
|
||||
const videoId = `test-section-bg-video-${Date.now()}`;
|
||||
const uploadDir = path.join(process.cwd(), "uploads");
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
await fs.writeFile(path.join(uploadDir, videoId), Buffer.from("dummy-section-video"));
|
||||
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "sec_bg_video",
|
||||
type: "section",
|
||||
props: {
|
||||
background: "default",
|
||||
paddingY: "md",
|
||||
columns: [{ id: "sec_bg_video_col_1", span: 12 }],
|
||||
backgroundVideoSrc: `/api/video/${videoId}`,
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "sec_bg_video_text",
|
||||
type: "text",
|
||||
sectionId: "sec_bg_video",
|
||||
columnId: "sec_bg_video_col_1",
|
||||
props: {
|
||||
text: "섹션 배경 비디오 내보내기 테스트",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "섹션 배경 비디오 내보내기 테스트",
|
||||
slug: "section-bg-video-export-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const buffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const videoEntry = zip.file(`videos/${videoId}`);
|
||||
expect(videoEntry).toBeTruthy();
|
||||
|
||||
const videoBytes = await videoEntry!.async("nodebuffer");
|
||||
expect(videoBytes.length).toBeGreaterThan(0);
|
||||
|
||||
const html = await zip.file("index.html")!.async("string");
|
||||
expect(html).toContain(`src="./videos/${videoId}`);
|
||||
expect(html).not.toContain(`/api/video/${videoId}`);
|
||||
expect(html).toContain("class=\"pb-section-bg-video\"");
|
||||
});
|
||||
|
||||
it("divider 블록은 정적 HTML에서 구분선 요소로 렌더되어야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
@@ -976,7 +1617,16 @@ describe("/api/export", () => {
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
fields: [],
|
||||
// v1 fields 설정이 있는 FormBlock 은 여전히 정적 HTML 폼으로 렌더되어야 한다.
|
||||
fields: [
|
||||
{
|
||||
id: "f1",
|
||||
name: "email",
|
||||
label: "이메일",
|
||||
type: "email",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
fieldIds: [],
|
||||
formWidthMode: "auto",
|
||||
backgroundColorCustom: "#111111",
|
||||
@@ -1072,6 +1722,77 @@ describe("/api/export", () => {
|
||||
expect(html).toContain("background-color:#123456");
|
||||
});
|
||||
|
||||
it("루트/섹션 레이아웃은 pb-root / pb-section-inner / pb-section-columns 구조를 사용해야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "root_layout_text",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "루트 레이아웃 텍스트",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "sec_layout_1",
|
||||
type: "section",
|
||||
props: {
|
||||
background: "default",
|
||||
paddingY: "md",
|
||||
columns: [{ id: "sec_layout_1_col_1", span: 12 }],
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "sec_layout_1_text",
|
||||
type: "text",
|
||||
sectionId: "sec_layout_1",
|
||||
columnId: "sec_layout_1_col_1",
|
||||
props: {
|
||||
text: "섹션 레이아웃 텍스트",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "레이아웃 구조 테스트",
|
||||
slug: "layout-structure-test",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(arrayBuffer);
|
||||
|
||||
const indexEntry = zip.file("index.html");
|
||||
expect(indexEntry).toBeTruthy();
|
||||
|
||||
const html = await indexEntry!.async("string");
|
||||
// 루트 텍스트는 pb-root / pb-root-inner 안에 포함되어야 한다.
|
||||
expect(html).toContain('<section class="pb-root"><div class="pb-root-inner">');
|
||||
expect(html).toContain("루트 레이아웃 텍스트");
|
||||
// 섹션 레이아웃 구조: pb-section + pb-section-inner + pb-section-columns + pb-section-column
|
||||
expect(html).toContain('class="pb-section');
|
||||
expect(html).toContain('class="pb-section-inner"');
|
||||
expect(html).toContain('class="pb-section-columns"');
|
||||
expect(html).toContain('class="pb-section-column"');
|
||||
expect(html).toContain("섹션 레이아웃 텍스트");
|
||||
});
|
||||
|
||||
it("formInput 블록의 textColorCustom 은 정적 HTML에서 input 텍스트 색상 스타일로 반영되어야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
@@ -1113,7 +1834,7 @@ describe("/api/export", () => {
|
||||
expect(indexEntry).toBeTruthy();
|
||||
|
||||
const html = await indexEntry!.async("string");
|
||||
expect(html).toContain("name=\"email\"");
|
||||
expect(html).toContain('name="email"');
|
||||
expect(html).toContain("color:#123456");
|
||||
});
|
||||
|
||||
@@ -1695,6 +2416,13 @@ describe("/api/export", () => {
|
||||
expect(css).toContain(".pb-list");
|
||||
expect(css).toContain(".pb-whitespace-pre-wrap");
|
||||
expect(css).toContain("color: var(--pb-color-text-strong)");
|
||||
// 레이아웃 관련 유틸리티 클래스도 고정해 둔다.
|
||||
expect(css).toContain(".pb-root-inner");
|
||||
expect(css).toContain("max-width: 72rem");
|
||||
expect(css).toContain(".pb-section-inner");
|
||||
expect(css).toContain(".pb-section-columns");
|
||||
expect(css).toContain("gap: 2rem");
|
||||
expect(css).toContain(".pb-section-column");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user