test(ai-character): 리소스 운영 E2E 보강
This commit is contained in:
164
tests/e2e/comments-test-support.ts
Normal file
164
tests/e2e/comments-test-support.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { Page, Route } from "@playwright/test";
|
||||
|
||||
import { apiBaseUrl } from "./api-base-url";
|
||||
const profileUrl = "https://cdn.example.com/luna.png";
|
||||
|
||||
type E2eComment = {
|
||||
comment: string;
|
||||
date: string;
|
||||
donationCan?: number;
|
||||
id: number;
|
||||
isSecret: boolean;
|
||||
languageCode?: string | null;
|
||||
nickname: string;
|
||||
parentId: number | null;
|
||||
profileUrl: string;
|
||||
replyCount: number;
|
||||
writerId: number;
|
||||
};
|
||||
|
||||
type CommentCreateBody = {
|
||||
readonly comment: string;
|
||||
readonly isSecret: boolean;
|
||||
readonly languageCode?: string | null;
|
||||
readonly parentId?: number | null;
|
||||
};
|
||||
|
||||
type CommentUpdateBody = { readonly comment: string };
|
||||
|
||||
export async function installServerModeCommentRoutes(page: Page): Promise<void> {
|
||||
const audioRoots = comments([
|
||||
[1101, null, 301, "팬", "오디오 팬 루트 댓글", 2],
|
||||
[1102, null, 101, "루나", "오디오 AI 루트 댓글", 0],
|
||||
], true);
|
||||
const audioReplies = comments([
|
||||
[1201, 1101, 301, "팬", "오디오 팬 답글", 0],
|
||||
[1202, 1101, 101, "루나", "오디오 AI 답글", 0],
|
||||
], true);
|
||||
const communityRoots = comments([
|
||||
[2101, null, 301, "팬", "커뮤니티 팬 루트 댓글", 2],
|
||||
[2102, null, 101, "루나", "커뮤니티 AI 루트 댓글", 0],
|
||||
], false);
|
||||
const communityReplies = comments([
|
||||
[2201, 2101, 301, "팬", "커뮤니티 팬 답글", 0],
|
||||
[2202, 2101, 101, "루나", "커뮤니티 AI 답글", 0],
|
||||
], false);
|
||||
let nextCommentId = 3000;
|
||||
|
||||
await page.route(`${apiBaseUrl}/admin/member/login`, async (route) => fulfillJson(route, { token: "admin-token", role: "ADMIN" }));
|
||||
await page.route(`${apiBaseUrl}/api/v2/admin/ai-characters?page=0&size=20`, async (route) => fulfillJson(route, characterList()));
|
||||
await page.route(`${apiBaseUrl}/api/v2/admin/ai-characters/101`, async (route) => fulfillJson(route, characterDetail()));
|
||||
await page.route(`${apiBaseUrl}/api/v2/admin/ai-characters/101/audio-contents/9001`, async (route) => fulfillJson(route, audioDetail()));
|
||||
await page.route(`${apiBaseUrl}/api/v2/admin/ai-characters/101/community-posts?page=0&size=20`, async (route) => fulfillJson(route, communityList()));
|
||||
await page.route(`${apiBaseUrl}/api/v2/admin/ai-characters/101/audio-contents/9001/comments**`, async (route) => {
|
||||
nextCommentId = await handleComments(route, audioRoots, audioReplies, nextCommentId, true, 1101);
|
||||
});
|
||||
await page.route(`${apiBaseUrl}/api/v2/admin/ai-characters/101/community-posts/7001/comments**`, async (route) => {
|
||||
nextCommentId = await handleComments(route, communityRoots, communityReplies, nextCommentId, false, 2101);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleComments(route: Route, roots: E2eComment[], replies: E2eComment[], nextId: number, isAudio: boolean, replyRootId: number): Promise<number> {
|
||||
const url = new URL(route.request().url());
|
||||
const method = route.request().method();
|
||||
if (url.pathname.endsWith(`/${replyRootId}/replies`)) {
|
||||
await fulfillJson(route, pageComments(replies));
|
||||
return nextId;
|
||||
}
|
||||
if (method === "GET") {
|
||||
await fulfillJson(route, pageComments(roots));
|
||||
return nextId;
|
||||
}
|
||||
if (method === "POST") {
|
||||
const body = parseCreateBody(route.request().postData());
|
||||
const collection = body.parentId === replyRootId ? replies : roots;
|
||||
const id = nextId + 1;
|
||||
collection.unshift({ id, parentId: body.parentId ?? null, writerId: 101, nickname: "루나", profileUrl, comment: body.comment, isSecret: body.isSecret, date: "2026-07-29T03:00:00Z", replyCount: 0, languageCode: isAudio ? body.languageCode ?? null : undefined, donationCan: isAudio ? 0 : undefined });
|
||||
await fulfillJson(route, null);
|
||||
return id;
|
||||
}
|
||||
if (method === "PUT") {
|
||||
updateComment(roots, replies, url.pathname, parseUpdateBody(route.request().postData()));
|
||||
} else {
|
||||
deleteComment(roots, replies, url.pathname);
|
||||
}
|
||||
await fulfillJson(route, null);
|
||||
return nextId;
|
||||
}
|
||||
|
||||
function parseCreateBody(value: string | null): CommentCreateBody {
|
||||
const data: unknown = JSON.parse(value ?? "{}");
|
||||
if (isRecord(data) && typeof data.comment === "string" && typeof data.isSecret === "boolean") {
|
||||
return { comment: data.comment, isSecret: data.isSecret, parentId: typeof data.parentId === "number" ? data.parentId : null, languageCode: typeof data.languageCode === "string" ? data.languageCode : null };
|
||||
}
|
||||
throw new Error("invalid comment create body");
|
||||
}
|
||||
|
||||
function parseUpdateBody(value: string | null): CommentUpdateBody {
|
||||
const data: unknown = JSON.parse(value ?? "{}");
|
||||
if (isRecord(data) && typeof data.comment === "string") {
|
||||
return { comment: data.comment };
|
||||
}
|
||||
throw new Error("invalid comment update body");
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function fulfillJson(route: Route, data: unknown): Promise<void> {
|
||||
return route.fulfill({ contentType: "application/json", json: { success: true, message: null, data, errorProperty: null } });
|
||||
}
|
||||
|
||||
function pageComments(items: readonly E2eComment[]) {
|
||||
return { totalCount: items.length, items };
|
||||
}
|
||||
|
||||
function updateComment(roots: readonly E2eComment[], replies: readonly E2eComment[], path: string, body: CommentUpdateBody): void {
|
||||
const id = Number(lastPathSegment(path));
|
||||
for (const comment of [...roots, ...replies]) {
|
||||
if (comment.id === id) {
|
||||
comment.comment = body.comment;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deleteComment(roots: E2eComment[], replies: E2eComment[], path: string): void {
|
||||
const id = Number(lastPathSegment(path));
|
||||
removeById(roots, id);
|
||||
removeById(replies, id);
|
||||
}
|
||||
|
||||
function removeById(comments: E2eComment[], id: number): void {
|
||||
const index = comments.findIndex((comment) => comment.id === id);
|
||||
if (index !== -1) {
|
||||
comments.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function lastPathSegment(path: string): string {
|
||||
const parts = path.split("/");
|
||||
|
||||
return parts[parts.length - 1] ?? "";
|
||||
}
|
||||
|
||||
function comments(rows: readonly (readonly [number, number | null, number, string, string, number])[], includeAudioFields: boolean): E2eComment[] {
|
||||
return rows.map(([id, parentId, writerId, nickname, comment, replyCount], index) => ({ id, parentId, writerId, nickname, profileUrl, comment, isSecret: false, date: `2026-07-29T0${index + 1}:00:00Z`, replyCount, languageCode: includeAudioFields ? "ko" : undefined, donationCan: includeAudioFields ? 0 : undefined }));
|
||||
}
|
||||
|
||||
function characterList() {
|
||||
return { totalCount: 1, content: [{ id: 101, name: "루나", imageUrl: null, description: "차분한 상담형 AI 캐릭터", gender: "여성", age: 24, mbti: "INFJ", speechStyle: "다정함", speechPattern: "존댓말", region: "KR", tags: ["상담", "힐링"], createdAt: "2026-07-28 10:00:00", updatedAt: "2026-07-28 11:00:00" }] };
|
||||
}
|
||||
|
||||
function characterDetail() {
|
||||
return { ...characterList().content[0], characterUUID: "character-uuid-101", systemPrompt: "친절하고 안전하게 답한다.", characterType: "Character", appearance: null, isActive: true, hobbies: [], values: [], goals: [], relationships: [], personalities: [], backgrounds: [], memories: [], originalWork: null };
|
||||
}
|
||||
|
||||
function audioDetail() {
|
||||
return { contentId: 9001, title: "달빛 상담 오디오", detail: "잠들기 전 듣는 상담 오디오", languageCode: "ko", coverImageUrl: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3C/svg%3E", contentUrl: "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=", themeStr: "힐링", tag: "상담,힐링", price: 1000, duration: "00:01", releaseDate: "2026-07-28T01:00:00Z", totalContentCount: 5, remainingContentCount: 4, orderSequence: 1, isActivePreview: true, isAdult: false, isMosaic: false, isOnlyRental: false, existOrdered: false, purchaseOption: "RENT_ONLY", orderType: null, remainingTime: "7일", creatorOtherContentList: [], sameThemeOtherContentList: [], isCommentAvailable: true, isLike: false, likeCount: 0, commentList: [], commentCount: 0, isPin: false, isAvailablePin: false, creator: { creatorId: 101, nickname: "루나", profileImageUrl: profileUrl, isFollowing: false, isFollow: false, isNotify: false }, previousContent: null, nextContent: null, buyerList: [], isAvailableUsePoint: true, translated: null };
|
||||
}
|
||||
|
||||
function communityList() {
|
||||
return { totalCount: 1, page: 0, size: 20, hasNext: false, items: [{ postId: 7001, creatorId: 101, creatorNickname: "루나", creatorProfileUrl: profileUrl, imageUrl: null, audioUrl: null, content: "오늘의 상담 기록입니다.", price: 0, date: "2026-07-28 10:00:00", dateUtc: "2026-07-28T01:00:00Z", isCommentAvailable: true, isAdult: false, isFixed: true, isLike: false, existOrdered: false, likeCount: 3, commentCount: 1, firstComment: null }] };
|
||||
}
|
||||
Reference in New Issue
Block a user