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 { 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 { const hasHorizontalOverflow = await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth); expect(hasHorizontalOverflow).toBe(false); } async function pressTabUntilFocused(page: Page, target: Locator): Promise { 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: "오디오 AI 루트 댓글 답글 작성" }).click(); const emptyReplies = page.getByRole("region", { name: "오디오 AI 루트 댓글 답글" }); await expect(emptyReplies.getByLabel("오디오 AI 루트 댓글에 답글")).toBeVisible(); await expect(emptyReplies.getByText("오디오 팬 루트 댓글", { exact: true })).toHaveCount(0); await emptyReplies.getByLabel("오디오 AI 루트 댓글에 답글").fill("오디오 첫 답글 생성"); await emptyReplies.getByRole("button", { name: "답글 등록" }).click(); await expect(emptyReplies.getByText("오디오 첫 답글 생성", { exact: true })).toBeVisible(); 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: 1102, 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 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("/community-posts/7001/comments")) { commentRequests.push({ body: request.postData(), method: request.method(), path: url.pathname, search: url.search }); } }); 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(); const writeReply = dialog.getByRole("button", { name: "커뮤니티 AI 루트 댓글 답글 작성" }); await expect(writeReply).toBeVisible(); await expect(writeReply).toHaveText("답글 작성"); await expectNoHorizontalOverflow(page); // When await dialog.getByLabel("새 댓글").fill("커뮤니티 루트 생성"); await dialog.getByRole("button", { name: "댓글 등록" }).click(); await expect(dialog.getByText("커뮤니티 루트 생성", { exact: true })).toBeVisible(); await writeReply.click(); const emptyReplies = dialog.getByRole("region", { name: "커뮤니티 AI 루트 댓글 답글" }); const emptyReplyInput = emptyReplies.getByLabel("커뮤니티 AI 루트 댓글에 답글"); await expect(emptyReplyInput).toBeVisible(); await expect(emptyReplies.getByText("커뮤니티 팬 루트 댓글", { exact: true })).toHaveCount(0); await expect.poll(() => commentRequests.filter((request) => request.method === "GET" && request.path.endsWith("/2102/replies") && request.search === "?page=0&size=20")).toHaveLength(1); await emptyReplyInput.fill(" 커뮤니티 첫 답글 생성 "); await emptyReplies.getByRole("button", { name: "답글 등록" }).click(); await expect(emptyReplyInput).toHaveValue(""); await emptyReplyInput.fill("커뮤니티 두 번째 답글 생성"); await emptyReplies.getByRole("button", { name: "답글 등록" }).click(); await expect(emptyReplyInput).toHaveValue(""); await expect.poll(() => commentRequests.filter((request) => request.method === "POST")).toEqual([ { body: JSON.stringify({ comment: "커뮤니티 루트 생성", parentId: null, isSecret: false }), method: "POST", path: "/api/v2/admin/ai-characters/101/community-posts/7001/comments", search: "" }, { body: JSON.stringify({ comment: "커뮤니티 첫 답글 생성", parentId: 2102, isSecret: false }), method: "POST", path: "/api/v2/admin/ai-characters/101/community-posts/7001/comments", search: "" }, { body: JSON.stringify({ comment: "커뮤니티 두 번째 답글 생성", parentId: 2102, isSecret: false }), method: "POST", path: "/api/v2/admin/ai-characters/101/community-posts/7001/comments", search: "" }, ]); await dialog.getByRole("button", { name: "닫기" }).click(); await expect(dialog).toBeHidden(); await page.getByRole("button", { name: "오늘의 상담 기록입니다. 게시글 열기" }).click(); await expect(dialog).toBeVisible(); await dialog.getByRole("button", { name: /^커뮤니티 AI 루트 댓글 답글 (작성|보기)$/ }).click(); await expect(emptyReplies).toBeVisible(); await expect(emptyReplies.getByText("커뮤니티 첫 답글 생성", { exact: true })).toBeVisible(); await expect(emptyReplies.getByText("커뮤니티 두 번째 답글 생성", { exact: true })).toBeVisible(); await expect(dialog.getByText("커뮤니티 첫 답글 생성", { exact: true })).toHaveCount(1); await expect(dialog.getByText("커뮤니티 두 번째 답글 생성", { exact: true })).toHaveCount(1); await expect(emptyReplies.getByRole("button", { name: /^(커뮤니티 첫 답글 생성|커뮤니티 두 번째 답글 생성) 답글 (작성|보기)$/ })).toHaveCount(0); const showReplies = dialog.getByRole("button", { name: "커뮤니티 팬 루트 댓글 답글 보기" }); await expect(showReplies).toBeEnabled(); await expect(showReplies).toHaveText("답글 보기"); await showReplies.click(); const replies = dialog.getByRole("region", { name: "커뮤니티 팬 루트 댓글 답글" }); await expect(replies).toBeVisible(); await expect(replies.getByText("커뮤니티 AI 답글", { exact: true })).toBeVisible(); await expect(replies.getByRole("button", { name: /^커뮤니티 AI 답글 답글 (작성|보기)$/ })).toHaveCount(0); await replies.getByLabel("커뮤니티 팬 루트 댓글에 답글").fill("커뮤니티 답글 생성"); await replies.getByRole("button", { name: "답글 등록" }).click(); const editReply = replies.getByRole("button", { name: "커뮤니티 AI 답글 수정" }); await expect(editReply).toHaveText("수정"); await editReply.click(); await replies.getByLabel("댓글 수정 내용").fill("커뮤니티 AI 답글 수정"); await replies.getByRole("button", { name: "수정 저장" }).click(); const deleteReply = replies.getByRole("button", { name: "커뮤니티 팬 답글 삭제" }); await expect(deleteReply).toHaveText("삭제"); await expect.poll(() => deleteReply.isEnabled()).toBe(true); await deleteReply.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("새 댓글"); const firstReplyAction = page.getByRole("button", { name: "오디오 AI 루트 댓글 답글 작성" }); // 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(); await pressTabUntilFocused(page, firstReplyAction); await page.keyboard.press("Enter"); await pressTabUntilFocused(page, page.getByRole("region", { name: "오디오 AI 루트 댓글 답글" }).getByLabel("오디오 AI 루트 댓글에 답글")); });