feat(ai-character): 시리즈 관리 기능 구현
This commit is contained in:
105
src/features/series/tests/series-form-crop.test.tsx
Normal file
105
src/features/series/tests/series-form-crop.test.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
import { SeriesForm } from "@/features/series/components/SeriesForm";
|
||||
import type { SeriesGenreItem, SeriesListItem } from "@/features/series/model/types";
|
||||
import type { CropSourceImage } from "@/shared/ui/image-crop-dialog";
|
||||
import type { ApiClient, ApiRequestOptions } from "@/shared/api/client";
|
||||
|
||||
type CapturedRequest = { readonly body?: BodyInit | null; readonly method?: string; readonly path: string };
|
||||
|
||||
const genres = [
|
||||
{ id: 77, genre: "로맨스", isAdult: false },
|
||||
{ id: 88, genre: "일상", isAdult: false },
|
||||
] as const satisfies readonly SeriesGenreItem[];
|
||||
|
||||
const existingSeries = {
|
||||
seriesId: 5001,
|
||||
title: "달빛 상담 시리즈",
|
||||
introduction: "밤마다 이어지는 상담 에피소드",
|
||||
coverImageUrl: "https://cdn.example.com/series/moon.png",
|
||||
publishedDaysOfWeek: ["SUN", "WED"],
|
||||
genreId: 77,
|
||||
isAdult: false,
|
||||
state: "PROCEEDING",
|
||||
isActive: true,
|
||||
writer: "스튜디오 루나",
|
||||
studio: "소다랩",
|
||||
} as const satisfies SeriesListItem;
|
||||
|
||||
function createClient(requests: CapturedRequest[]): ApiClient {
|
||||
return { async request<Data>(options: ApiRequestOptions<Data>): Promise<Data> { requests.push({ body: options.body, method: options.method, path: options.path }); return options.responseSchema.parse(null); } };
|
||||
}
|
||||
|
||||
function createImage(name = "series.png"): File {
|
||||
return new File(["image"], name, { type: "image/png" });
|
||||
}
|
||||
|
||||
function renderEditForm(requests: CapturedRequest[], createCropSource: (file: File) => Promise<CropSourceImage>) {
|
||||
window.history.pushState({}, "", "/ai-characters/101/series/5001/edit");
|
||||
render(<SeriesForm apiClient={createClient(requests)} characterId="101" createCropSource={createCropSource} genres={genres} mode="edit" renderCrop={(request) => Promise.resolve(request.file)} series={existingSeries} />);
|
||||
}
|
||||
|
||||
function requireFile(part: FormDataEntryValue | null): File {
|
||||
if (part instanceof File) {
|
||||
return part;
|
||||
}
|
||||
throw new TypeError("Expected File part");
|
||||
}
|
||||
|
||||
test("SeriesForm blocks edit save while replacement image crop source is preparing", async () => {
|
||||
const requests: CapturedRequest[] = [];
|
||||
renderEditForm(requests, () => new Promise(() => undefined));
|
||||
|
||||
fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [createImage("pending.png")] } });
|
||||
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "저장" })).toBeDisabled());
|
||||
fireEvent.submit(screen.getByRole("form", { name: "시리즈 수정 입력 화면" }));
|
||||
expect(requests.filter((request) => request.method === "PUT")).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("SeriesForm ignores stale image crop sources and saves the latest applied replacement", async () => {
|
||||
const requests: CapturedRequest[] = [];
|
||||
const cropResolvers = new Map<string, (source: CropSourceImage) => void>();
|
||||
renderEditForm(requests, (file) => new Promise((resolve) => cropResolvers.set(file.name, resolve)));
|
||||
const staleFile = createImage("stale.png");
|
||||
const freshFile = createImage("fresh.png");
|
||||
|
||||
fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [staleFile] } });
|
||||
fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [freshFile] } });
|
||||
cropResolvers.get("fresh.png")?.({ file: freshFile, height: 1414, previewUrl: "blob:fresh", width: 1000 });
|
||||
fireEvent.click(await screen.findByRole("button", { name: "적용" }));
|
||||
await screen.findByText("fresh.png");
|
||||
cropResolvers.get("stale.png")?.({ file: staleFile, height: 1414, previewUrl: "blob:stale", width: 1000 });
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole("dialog", { name: "이미지 crop" })).not.toBeInTheDocument());
|
||||
fireEvent.submit(screen.getByRole("form", { name: "시리즈 수정 입력 화면" }));
|
||||
await waitFor(() => expect(window.location.pathname).toBe("/ai-characters/101/series/5001"));
|
||||
const updateBody = requests.find((request) => request.method === "PUT")?.body as FormData;
|
||||
expect(requireFile(updateBody.get("image")).name).toBe("fresh.png");
|
||||
});
|
||||
|
||||
test("SeriesForm keeps existing edit image when replacement crop is canceled", async () => {
|
||||
const requests: CapturedRequest[] = [];
|
||||
renderEditForm(requests, (file) => Promise.resolve({ file, height: 1414, previewUrl: "blob:series-preview", width: 1000 }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [createImage("replacement.png")] } });
|
||||
const cropDialog = await screen.findByRole("dialog", { name: "이미지 crop" });
|
||||
fireEvent.click(within(cropDialog).getByRole("button", { name: "취소" }));
|
||||
fireEvent.submit(screen.getByRole("form", { name: "시리즈 수정 입력 화면" }));
|
||||
|
||||
await waitFor(() => expect(window.location.pathname).toBe("/ai-characters/101/series/5001"));
|
||||
const updateBody = requests.find((request) => request.method === "PUT")?.body as FormData;
|
||||
expect(updateBody.get("image")).toBeNull();
|
||||
});
|
||||
|
||||
test.each(["create", "edit"] as const)("SeriesForm shows an image preparation error when %s preview creation rejects", async (mode) => {
|
||||
const requests: CapturedRequest[] = [];
|
||||
window.history.pushState({}, "", mode === "create" ? "/ai-characters/101/series/new" : "/ai-characters/101/series/5001/edit");
|
||||
render(<SeriesForm apiClient={createClient(requests)} characterId="101" createCropSource={() => Promise.reject(new Error("preview failed"))} genres={genres} mode={mode} renderCrop={(request) => Promise.resolve(request.file)} series={mode === "edit" ? existingSeries : undefined} />);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [createImage("broken.png")] } });
|
||||
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("이미지 미리보기 준비에 실패했습니다.");
|
||||
expect(screen.queryByRole("dialog", { name: "이미지 crop" })).not.toBeInTheDocument();
|
||||
});
|
||||
Reference in New Issue
Block a user