diff --git a/web/src/components/community/BodyComposer.test.tsx b/web/src/components/community/BodyComposer.test.tsx new file mode 100644 index 000000000..15776a75e --- /dev/null +++ b/web/src/components/community/BodyComposer.test.tsx @@ -0,0 +1,91 @@ +// Write, Preview, Post: the comment went up and the field came back as an +// empty preview box, the Preview tab still picked and greyed out, no textarea +// anywhere. Writing a second comment meant finding "Write" first. A preview +// of nothing is what the disabled tab is there to prevent, so the field goes +// back to Write when its text goes. + +import { fireEvent, render, screen } from "@testing-library/react"; +import { useState } from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import BodyComposer from "./BodyComposer"; +import CommentSection from "./CommentSection"; + +const refresh = vi.fn(); +vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh, push: vi.fn() }) })); +vi.mock("@/lib/community/auth-client", () => ({ + authClient: { + useSession: () => ({ data: { user: { id: "u1", username: "ada" } }, isPending: false }), + }, +})); + +function Field({ initial }: { initial: string }) { + const [value, setValue] = useState(initial); + return ( + <> + + + + ); +} + +const tab = (name: string) => screen.getByRole("tab", { name }); + +describe("BodyComposer", () => { + it("goes back to Write when its text is cleared while previewing", () => { + render(); + fireEvent.click(tab("Preview")); + expect(screen.queryByRole("textbox", { name: "Comment" })).toBeNull(); + expect(tab("Preview").getAttribute("aria-selected")).toBe("true"); + + fireEvent.click(screen.getByRole("button", { name: "clear" })); + + expect((screen.getByRole("textbox", { name: "Comment" }) as HTMLTextAreaElement).value).toBe(""); + expect(tab("Write").getAttribute("aria-selected")).toBe("true"); + expect(tab("Preview").getAttribute("aria-selected")).toBe("false"); + expect((tab("Preview") as HTMLButtonElement).disabled).toBe(true); + }); + + it("stays on Write as the next text is typed", () => { + render(); + fireEvent.click(tab("Preview")); + fireEvent.click(screen.getByRole("button", { name: "clear" })); + + fireEvent.change(screen.getByRole("textbox", { name: "Comment" }), { target: { value: "s" } }); + + // One letter in, the field is still a textarea; the old preview does not + // come back on its own. + expect((screen.getByRole("textbox", { name: "Comment" }) as HTMLTextAreaElement).value).toBe("s"); + expect(tab("Write").getAttribute("aria-selected")).toBe("true"); + expect((tab("Preview") as HTMLButtonElement).disabled).toBe(false); + }); + + it("keeps the preview while the text is there", () => { + render(); + fireEvent.click(tab("Preview")); + expect(screen.getByText("still here")).toBeTruthy(); + expect(screen.queryByRole("textbox", { name: "Comment" })).toBeNull(); + }); +}); + +describe("posting from Preview", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(Response.json({ ok: true }, { status: 201 }))); + }); + afterEach(() => { + vi.unstubAllGlobals(); + refresh.mockReset(); + }); + + it("brings the comment field back empty, on Write", async () => { + render(); + fireEvent.change(screen.getByRole("textbox", { name: "Comment" }), { target: { value: "nice *knobs*" } }); + fireEvent.click(tab("Preview")); + fireEvent.click(screen.getByRole("button", { name: "Post" })); + + await vi.waitFor(() => expect(refresh).toHaveBeenCalled()); + expect((screen.getByRole("textbox", { name: "Comment" }) as HTMLTextAreaElement).value).toBe(""); + expect(tab("Write").getAttribute("aria-selected")).toBe("true"); + }); +}); diff --git a/web/src/components/community/BodyComposer.tsx b/web/src/components/community/BodyComposer.tsx index 556e16ca5..0ce34ac6e 100644 --- a/web/src/components/community/BodyComposer.tsx +++ b/web/src/components/community/BodyComposer.tsx @@ -32,6 +32,11 @@ export default function BodyComposer({ autoFocus?: boolean; }) { const [previewing, setPreviewing] = useState(false); + // Posting from Preview clears the text under it. Left there, the field was + // a preview of nothing with its tab greyed out and no textarea to write the + // next comment in. Adjusted during render, guarded, like AuthStatus's latch. + const hasText = value.trim().length > 0; + if (previewing && !hasText) setPreviewing(false); const ref = useAutoGrow(previewing ? "" : value); return ( @@ -53,7 +58,7 @@ export default function BodyComposer({ data-active={previewing} // Nothing to preview is worth saying out loud rather than showing an // empty box that looks like a broken renderer. - disabled={value.trim().length === 0} + disabled={!hasText} onClick={() => setPreviewing(true)} > Preview