feat(ai-character): 리소스 관리 기반 정비
This commit is contained in:
130
src/layouts/CharacterWorkspaceLayout.test.tsx
Normal file
130
src/layouts/CharacterWorkspaceLayout.test.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { App } from "@/app/App";
|
||||
import { authSessionStorage } from "@/features/auth/model/auth-session-storage";
|
||||
import { server } from "@/shared/test/server";
|
||||
|
||||
const apiBaseUrl = "https://api.example.com";
|
||||
const listEnvelope = {
|
||||
success: true,
|
||||
message: null,
|
||||
data: { totalCount: 1, content: [] },
|
||||
errorProperty: null,
|
||||
} as const;
|
||||
const activeDetail = {
|
||||
id: 101,
|
||||
characterUUID: "character-uuid-101",
|
||||
name: "루나",
|
||||
imageUrl: "https://cdn.example.com/luna.png",
|
||||
description: "차분한 상담형 AI 캐릭터",
|
||||
systemPrompt: "친절하게 답한다.",
|
||||
characterType: "Character",
|
||||
age: 24,
|
||||
gender: "여성",
|
||||
mbti: "INFJ",
|
||||
speechPattern: "존댓말",
|
||||
speechStyle: "다정함",
|
||||
appearance: "푸른 머리와 밝은 눈",
|
||||
region: "KR",
|
||||
isActive: true,
|
||||
tags: ["상담"],
|
||||
hobbies: [],
|
||||
values: [],
|
||||
goals: [],
|
||||
relationships: [],
|
||||
personalities: [],
|
||||
backgrounds: [],
|
||||
memories: [],
|
||||
originalWork: { id: 7, imageUrl: null, title: "달빛 상담소" },
|
||||
} as const;
|
||||
const inactiveDetail = { ...activeDetail, id: 202, characterUUID: "character-uuid-202", name: "미카", isActive: false, originalWork: null } as const;
|
||||
|
||||
function saveAdminSession() {
|
||||
authSessionStorage.save({ token: "admin-token", role: "ADMIN" });
|
||||
}
|
||||
|
||||
function useWorkspaceHandlers(detailStatus: 200 | 400 | 404 | 500 = 200) {
|
||||
server.use(
|
||||
http.get(`${apiBaseUrl}/api/v2/admin/ai-characters`, () => HttpResponse.json(listEnvelope)),
|
||||
http.get(`${apiBaseUrl}/api/v2/admin/ai-characters/:characterId`, ({ params }) => {
|
||||
if (detailStatus !== 200) {
|
||||
return HttpResponse.json({ success: false, message: "상세 조회 실패", data: null, errorProperty: null }, { status: detailStatus });
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
message: null,
|
||||
data: params.characterId === "202" ? inactiveDetail : activeDetail,
|
||||
errorProperty: null,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
window.history.replaceState({}, "", "/");
|
||||
});
|
||||
|
||||
test("Character workspace restores an active detail from a deep link", async () => {
|
||||
// Given
|
||||
saveAdminSession();
|
||||
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
|
||||
useWorkspaceHandlers();
|
||||
window.history.pushState({}, "", "/ai-characters/101");
|
||||
|
||||
// When
|
||||
render(<App />);
|
||||
|
||||
// Then
|
||||
expect(await screen.findByRole("heading", { name: "루나" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("navigation", { name: "캐릭터 워크스페이스 브레드크럼" })).toHaveTextContent(/AI 캐릭터.*루나/);
|
||||
expect(screen.getByLabelText("상태: 공개")).toBeInTheDocument();
|
||||
expect(screen.getByText("characterId: 101")).toBeInTheDocument();
|
||||
expect(screen.getByText("character-uuid-101")).toBeInTheDocument();
|
||||
const workspaceNavigation = screen.getByRole("navigation", { name: "캐릭터 워크스페이스 내비게이션" });
|
||||
expect(workspaceNavigation).toHaveTextContent(/프로필.*오디오 콘텐츠.*시리즈.*커뮤니티.*FanTalk/);
|
||||
expect(screen.queryByRole("tablist")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("tab", { name: "프로필" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "프로필" })).toHaveAttribute("aria-current", "page");
|
||||
expect(screen.getByRole("img", { name: "루나 프로필 이미지" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Character workspace shows read-only policy for inactive detail without mutation entry points", async () => {
|
||||
// Given
|
||||
saveAdminSession();
|
||||
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
|
||||
useWorkspaceHandlers();
|
||||
window.history.pushState({}, "", "/ai-characters/202");
|
||||
|
||||
// When
|
||||
render(<App />);
|
||||
|
||||
// Then
|
||||
expect(await screen.findByRole("heading", { name: "미카" })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("상태: 비활성")).toBeInTheDocument();
|
||||
expect(screen.getByText("characterId: 202")).toBeInTheDocument();
|
||||
expect(screen.getByRole("alert")).toHaveTextContent("비활성 캐릭터는 읽기 전용입니다.");
|
||||
expect(screen.getByRole("alert")).toHaveTextContent("조회만 가능하며 생성, 수정, 비활성화 같은 쓰기 작업은 실행되지 않습니다.");
|
||||
expect(screen.getByRole("alert")).toHaveTextContent("복원 기능을 지원하지 않습니다.");
|
||||
expect(screen.getByRole("alert")).toHaveTextContent("영구 삭제와 삭제 후 복구도 제공하지 않습니다.");
|
||||
expect(screen.queryByText(/P3-T2|Task|범위/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "저장" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "비활성화" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test.each([400, 404, 500] as const)("Character workspace uses the common error state for detail %s", async (status) => {
|
||||
// Given
|
||||
saveAdminSession();
|
||||
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
|
||||
useWorkspaceHandlers(status);
|
||||
window.history.pushState({}, "", "/ai-characters/999");
|
||||
|
||||
// When
|
||||
render(<App />);
|
||||
|
||||
// Then
|
||||
await waitFor(() => expect(screen.getByRole("alert")).toHaveTextContent("상세 조회 실패"));
|
||||
expect(screen.queryByText("비활성 캐릭터는 읽기 전용입니다.")).not.toBeInTheDocument();
|
||||
});
|
||||
Reference in New Issue
Block a user