test(ai-character): 리소스 운영 E2E 보강

This commit is contained in:
Yu Sung
2026-08-01 01:30:52 +09:00
parent 3faca90135
commit 3759abf8b7
16 changed files with 1962 additions and 62 deletions

133
tests/e2e/comments.spec.ts Normal file
View File

@@ -0,0 +1,133 @@
import { expect, test } from "@playwright/test";
import type { Locator, Page } from "@playwright/test";
import { installServerModeCommentRoutes } from "./comments-test-support";
async function loginThroughMockMode(page: Page): Promise<void> {
await installServerModeCommentRoutes(page);
await page.goto("/login");
await page.getByLabel("이메일").fill("admin@test.com");
await page.getByLabel("비밀번호").fill("password");
await page.getByRole("button", { name: "로그인" }).click();
await expect(page).toHaveURL(/\/ai-characters$/);
}
async function expectNoHorizontalOverflow(page: Page): Promise<void> {
const hasHorizontalOverflow = await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth);
expect(hasHorizontalOverflow).toBe(false);
}
async function pressTabUntilFocused(page: Page, target: Locator): Promise<void> {
for (let attempt = 0; attempt < 60; attempt += 1) {
if (await target.evaluate((element) => element === document.activeElement || element.contains(document.activeElement))) {
return;
}
await page.keyboard.press("Tab");
}
await expect(target).toBeFocused();
}
test("Audio comments create reply edit AI rows and delete fan or AI rows through two-level endpoints", async ({ page }) => {
// Given
const commentRequests: { readonly body: string | null; readonly method: string; readonly path: string; readonly search: string }[] = [];
page.on("request", (request) => {
const url = new URL(request.url());
if (url.pathname.includes("/audio-contents/9001/comments")) {
commentRequests.push({ body: request.postData(), method: request.method(), path: url.pathname, search: url.search });
}
});
await loginThroughMockMode(page);
// When
await page.goto("/ai-characters/101/audio-contents/9001");
// Then
await expect(page.getByRole("heading", { name: "댓글 관리" })).toBeVisible();
await expect(page.getByText("오디오 팬 루트 댓글", { exact: true })).toBeVisible();
expect(commentRequests).toContainEqual({ body: null, method: "GET", path: "/api/v2/admin/ai-characters/101/audio-contents/9001/comments", search: "?page=0&size=20" });
// When
await page.getByLabel("새 댓글").fill("오디오 루트 생성");
await page.getByRole("button", { name: "댓글 등록" }).click();
await page.getByRole("button", { name: "오디오 팬 루트 댓글 답글 보기" }).click();
const replies = page.getByRole("region", { name: "오디오 팬 루트 댓글 답글" });
await replies.getByLabel("오디오 팬 루트 댓글에 답글").fill("오디오 답글 생성");
await replies.getByRole("button", { name: "답글 등록" }).click();
await expect(replies.getByText("오디오 답글 생성", { exact: true })).toBeVisible();
await replies.getByRole("button", { name: "오디오 AI 답글 수정" }).click();
await replies.getByLabel("댓글 수정 내용").fill("오디오 AI 답글 수정");
await replies.getByRole("button", { name: "수정 저장" }).click();
await expect(replies.getByText("오디오 답글 생성", { exact: true })).toBeVisible();
await expect.poll(() => commentRequests.some((request) => request.method === "PUT" && request.path.endsWith("/1202"))).toBe(true);
await replies.getByRole("button", { name: "오디오 팬 답글 삭제" }).click();
await expect.poll(() => commentRequests.some((request) => request.method === "DELETE" && request.path.endsWith("/1201"))).toBe(true);
await page.getByRole("button", { name: "오디오 AI 루트 댓글 삭제" }).click();
// Then
await expect(page.getByText("오디오 AI 루트 댓글", { exact: true })).toBeHidden();
await expect(replies.getByText("오디오 답글 생성", { exact: true })).toBeVisible();
expect(commentRequests).toContainEqual({ body: JSON.stringify({ comment: "오디오 루트 생성", parentId: null, isSecret: false, languageCode: null }), method: "POST", path: "/api/v2/admin/ai-characters/101/audio-contents/9001/comments", search: "" });
expect(commentRequests).toContainEqual({ body: JSON.stringify({ comment: "오디오 답글 생성", parentId: 1101, isSecret: false, languageCode: null }), method: "POST", path: "/api/v2/admin/ai-characters/101/audio-contents/9001/comments", search: "" });
expect(commentRequests).toContainEqual({ body: JSON.stringify({ comment: "오디오 AI 답글 수정" }), method: "PUT", path: "/api/v2/admin/ai-characters/101/audio-contents/9001/comments/1202", search: "" });
expect(commentRequests).toContainEqual({ body: null, method: "DELETE", path: "/api/v2/admin/ai-characters/101/audio-contents/9001/comments/1201", search: "" });
expect(commentRequests).toContainEqual({ body: null, method: "DELETE", path: "/api/v2/admin/ai-characters/101/audio-contents/9001/comments/1102", search: "" });
expect(commentRequests.some((request) => request.method === "PUT" && request.path.endsWith("/1101"))).toBe(false);
});
test("Community sheet comments keep two-level controls usable at 320px", async ({ page }) => {
// Given
await page.setViewportSize({ width: 320, height: 640 });
await loginThroughMockMode(page);
// When
await page.goto("/ai-characters/101/community-posts");
await page.getByRole("button", { name: "오늘의 상담 기록입니다. 게시글 열기" }).click();
const dialog = page.getByRole("dialog", { name: "커뮤니티 게시글" });
// Then
await expect(dialog.getByRole("heading", { name: "댓글 관리" })).toBeVisible();
await expect(dialog.getByLabel("새 댓글")).toBeInViewport();
await expect(dialog.getByText("커뮤니티 팬 루트 댓글", { exact: true })).toBeVisible();
await expectNoHorizontalOverflow(page);
// When
await dialog.getByLabel("새 댓글").fill("커뮤니티 루트 생성");
await dialog.getByRole("button", { name: "댓글 등록" }).click();
await expect(dialog.getByText("커뮤니티 루트 생성", { exact: true })).toBeVisible();
const showReplies = dialog.getByRole("button", { name: "커뮤니티 팬 루트 댓글 답글 보기" });
await expect(showReplies).toBeEnabled();
await showReplies.click();
const replies = dialog.getByRole("region", { name: "커뮤니티 팬 루트 댓글 답글" });
await expect(replies).toBeVisible();
await replies.getByLabel("커뮤니티 팬 루트 댓글에 답글").fill("커뮤니티 답글 생성");
await replies.getByRole("button", { name: "답글 등록" }).click();
await replies.getByRole("button", { name: "커뮤니티 AI 답글 수정" }).click();
await replies.getByLabel("댓글 수정 내용").fill("커뮤니티 AI 답글 수정");
await replies.getByRole("button", { name: "수정 저장" }).click();
await expect.poll(() => replies.getByRole("button", { name: "커뮤니티 팬 답글 삭제" }).isEnabled()).toBe(true);
await replies.getByRole("button", { name: "커뮤니티 팬 답글 삭제" }).click();
// Then
await expect(replies.getByText("커뮤니티 팬 답글", { exact: true })).toBeHidden();
await expectNoHorizontalOverflow(page);
});
test("keyboard-only Audio comment flow reaches form and reply controls", async ({ browserName, page }) => {
test.skip(browserName === "webkit", "WebKit does not consistently tab-focus controls in this keyboard path.");
// Given
await page.setViewportSize({ width: 1280, height: 900 });
await loginThroughMockMode(page);
await page.goto("/ai-characters/101/audio-contents/9001");
const rootInput = page.getByLabel("새 댓글");
// When / Then
await pressTabUntilFocused(page, rootInput);
await page.keyboard.type("키보드 루트 댓글");
await pressTabUntilFocused(page, page.getByRole("button", { name: "댓글 등록" }));
await page.keyboard.press("Enter");
await expect(page.getByText("키보드 루트 댓글", { exact: true })).toBeVisible();
});