// @vitest-environment happy-dom import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { beforeEach, afterEach, describe, expect, it, vi } from "vitest"; import type { CommentItem, CommentViewer } from "@/lib/comments/types"; vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) })); import { CommentCard } from "./comment-card"; const giver = { id: "admin", name: "Admin", image: "https://profile.example/admin.png" }; let root: Root; let container: HTMLDivElement; let item: CommentItem; const fetchMock = vi.fn(); const onChange = vi.fn(); beforeEach(() => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); vi.spyOn(HTMLImageElement.prototype, "complete", "get").mockReturnValue(true); vi.spyOn(HTMLImageElement.prototype, "naturalWidth", "get").mockReturnValue(64); fetchMock.mockReset().mockResolvedValue(new Response(null, { status: 204 })); vi.stubGlobal("fetch", fetchMock); onChange.mockReset().mockResolvedValue(undefined); item = { id: "comment", rootId: null, replyToId: null, replyToName: null, authorName: "Reader", authorImage: null, authorAdmin: false, text: "Comment", images: [], version: 1, createdAt: "2026-10-08T12:00:00Z", editedAt: "2026-10-08T12:00:00Z", hidden: false, ownHidden: false, deleted: false, hearted: true, heartedBy: giver, likes: 0, reaction: 0, replyCount: 0, hasReplies: false, canEdit: false, canInteract: true, canModerate: false, target: "guide:test", targetName: "Test", targetImage: null, href: "/test/comment", }; container = document.createElement("div"); document.body.append(container); root = createRoot(container); }); afterEach(async () => { await act(async () => root.unmount()); container.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); async function mount(viewer: CommentViewer | null = null) { await act(async () => root.render()); } function heartAvatar() { return container.querySelector('[data-slot="avatar"][aria-label="Admin ถูกใจความคิดเห็นนี้"]'); } describe("admin profile hearts on comments", () => { it("shows the heart giver's profile and a small filled heart at its bottom right", async () => { await mount(); const avatar = heartAvatar(); expect(avatar?.querySelector("img")?.getAttribute("src")).toBe(giver.image); const badge = avatar?.querySelector('[data-slot="avatar-badge"]'); expect(badge?.classList.contains("-right-0.5")).toBe(true); expect(badge?.classList.contains("-bottom-0.5")).toBe(true); expect(badge?.querySelector("svg")?.getAttribute("fill")).toBe("currentColor"); expect(container.textContent).not.toContain("ผู้ดูแลถูกใจ"); }); it("keeps the giver's initials and heart when the profile picture is unavailable", async () => { item.heartedBy = { ...giver, image: null }; await mount(); expect(heartAvatar()?.querySelector('[data-slot="avatar-fallback"]')?.textContent).toBe("Ad"); expect(heartAvatar()?.querySelector('[data-slot="avatar-badge"]')).not.toBeNull(); }); it.each(["not hearted", "hidden", "deleted"])("hides the public profile heart when the comment is %s", async (state) => { if (state === "not hearted") item.hearted = false; else if (state === "hidden") item.hidden = true; else item.deleted = true; await mount(); expect(heartAvatar()).toBeNull(); }); it("preserves the admin action to remove a heart", async () => { await mount({ ...giver, admin: true }); const button = container.querySelector('button[aria-label="ยกเลิกหัวใจจากผู้ดูแล"]'); expect(button?.getAttribute("aria-pressed")).toBe("true"); expect(button?.querySelector('[data-slot="avatar-badge"]')).not.toBeNull(); await act(async () => button!.querySelector('[data-slot="avatar"]')!.click()); expect(fetchMock).toHaveBeenCalledWith("/api/comments/comment/heart", expect.objectContaining({ method: "PUT", body: '{"hearted":false}' })); expect(onChange).toHaveBeenCalledOnce(); expect(heartAvatar()).toBeNull(); }); it("shows the acting admin's profile immediately when adding a heart", async () => { item.hearted = false; item.heartedBy = null; await mount({ ...giver, admin: true }); await act(async () => container.querySelector('button[aria-label="ให้หัวใจจากผู้ดูแล"]')!.click()); expect(heartAvatar()?.querySelector("img")?.getAttribute("src")).toBe(giver.image); expect(fetchMock).toHaveBeenCalledWith("/api/comments/comment/heart", expect.objectContaining({ body: '{"hearted":true}' })); }); });