Files
gitmost/apps/client/src/features/comment/components/comment-list-with-tabs.test.tsx
T
agent_coder a4fc6c7f64 fix(comment): underline mark + draft-surviving tabs + test coverage (#349 review F1-F4)
- F1: render the `underline` mark statically (StarterKit v3 enables Underline;
  comment-editor does not disable it) — an underlined comment no longer degrades
  the whole comment to the read-only editor fallback. renderMarks gains a
  `case "underline" -> <u>`, mirroring the other marks (+ test).
- F2: keep the Open tab panel mounted (`Tabs.Panel value="open" keepMounted`)
  while the heavy Resolved panel still unmounts (`Tabs keepMounted={false}`). A
  per-panel keepMounted overrides the parent's `false` (Mantine 8 TabsPanel), so
  an in-progress reply draft / edit in the Open panel survives an
  Open->Resolved->Open switch, keeping the micro-opt of not mounting the large
  Resolved list.
- F3: cover edit->save->re-render in comment-list-item.test.tsx — save calls
  mutateAsync with JSON.stringify(editContentRef) and a new comment.content prop
  updates the visible body; cancel restores the static body without mutating;
  clearing editContentRef after cancel.
- F4: extract childrenByParent grouping into an exported pure
  `buildChildrenByParent(items)` (unit-tested: nesting, orphan reply, sibling
  order) + new comment-list-with-tabs.test.tsx covering the lazy reply-editor
  activation (stub -> click/focus/Enter mounts the editor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 20:48:21 +03:00

109 lines
4.0 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { MantineProvider } from "@mantine/core";
import { IComment } from "@/features/comment/types/comment.types";
// matchMedia (read by MantineProvider) is stubbed globally in vitest.setup.ts.
// CommentEditor pulls in the full TipTap editor stack; replace it with a stub so
// the lazy reply editor's mount transition can be observed without the editor.
vi.mock("@/features/comment/components/comment-editor", () => ({
default: () => <div data-testid="comment-editor" />,
}));
// page-query -> main.tsx (createRoot) is a module side effect; stub the queries
// pulled in transitively so importing the module is side-effect free.
vi.mock("@/features/page/queries/page-query.ts", () => ({
usePageQuery: () => ({ data: undefined, isLoading: false, isError: false }),
}));
vi.mock("@/features/share/queries/share-query.ts", () => ({
useSharePageQuery: () => ({ data: undefined }),
}));
// space-query -> main.tsx (createRoot) is another module side effect; stub it.
vi.mock("@/features/space/queries/space-query.ts", () => ({
useGetSpaceBySlugQuery: () => ({ data: undefined }),
}));
import {
buildChildrenByParent,
CommentEditorWithActions,
} from "./comment-list-with-tabs";
const c = (id: string, parentCommentId: string | null = null): IComment =>
({ id, parentCommentId }) as IComment;
describe("buildChildrenByParent (childrenByParent grouping)", () => {
it("returns an empty map for undefined or empty input", () => {
expect(buildChildrenByParent(undefined).size).toBe(0);
expect(buildChildrenByParent([]).size).toBe(0);
});
it("does not index a top-level comment (parentCommentId null)", () => {
const map = buildChildrenByParent([c("p1", null)]);
expect(map.size).toBe(0);
expect(map.has("p1")).toBe(false);
});
it("groups replies under the correct parent, including reply-to-reply nesting", () => {
const p1 = c("p1", null);
const r1 = c("r1", "p1");
const r2 = c("r2", "r1"); // a reply to a reply
const map = buildChildrenByParent([p1, r1, r2]);
expect(map.get("p1")).toEqual([r1]);
expect(map.get("r1")).toEqual([r2]);
// The top-level comment itself is never a key.
expect(map.has("p1") && map.get("p1")?.length).toBe(1);
});
it("still groups a reply whose parent is not present in items", () => {
const orphan = c("o1", "missing-parent");
const map = buildChildrenByParent([orphan]);
expect(map.get("missing-parent")).toEqual([orphan]);
});
it("preserves insertion order among sibling replies", () => {
const map = buildChildrenByParent([
c("a", "p1"),
c("b", "p1"),
c("d", "p1"),
]);
expect(map.get("p1")?.map((x) => x.id)).toEqual(["a", "b", "d"]);
});
});
function renderReplyEditor() {
return render(
<MantineProvider>
<CommentEditorWithActions commentId="c-1" onSave={vi.fn()} />
</MantineProvider>,
);
}
describe("CommentEditorWithActions — lazy reply editor activation", () => {
it("shows only the stub initially (no editor instance mounted)", () => {
renderReplyEditor();
expect(screen.getByRole("button")).toBeDefined();
expect(screen.queryByTestId("comment-editor")).toBeNull();
});
it("mounts the real editor when the stub is clicked and keeps it mounted", () => {
renderReplyEditor();
fireEvent.click(screen.getByRole("button"));
expect(screen.getByTestId("comment-editor")).toBeDefined();
// The stub button is replaced by the editor subtree.
expect(screen.queryByRole("button")).toBeNull();
});
it("mounts the editor when the stub receives focus", () => {
renderReplyEditor();
fireEvent.focus(screen.getByRole("button"));
expect(screen.getByTestId("comment-editor")).toBeDefined();
});
it("mounts the editor on Enter keydown of the stub", () => {
renderReplyEditor();
fireEvent.keyDown(screen.getByRole("button"), { key: "Enter" });
expect(screen.getByTestId("comment-editor")).toBeDefined();
});
});