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(); // 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(); // 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(); // Then await waitFor(() => expect(screen.getByRole("alert")).toHaveTextContent("상세 조회 실패")); expect(screen.queryByText("비활성 캐릭터는 읽기 전용입니다.")).not.toBeInTheDocument(); });