1import { EnvironmentId, ThreadId } from "@t3tools/contracts";
2import { expect, it, vi } from "vite-plus/test";
3import { userEvent } from "vite-plus/test/browser";
4
5const persisted = vi.hoisted(() => ({ title: "" }));
6const openMenu = vi.hoisted(() => vi.fn());
7vi.mock(import("../../state/session"), async (importOriginal) => ({
8 ...(await importOriginal()),
9 useEnvironmentScope: () => true,
10 readEnvironmentScope: () => true,
11}));
12vi.mock(import("../../state/use-orchestration-command"), async (importOriginal) => ({
13 ...(await importOriginal()),
14 useOrchestrationCommand: (() =>
15 async ({ input }: { input: { title: string } }) => {
16 persisted.title = input.title;
17 return { _tag: "Success" };
18 }) as never,
19}));
20vi.mock(import("~/hooks/useThreadActionMenu"), () => ({
21 useThreadActionMenu: () => ({ openMenu, closeMenu: () => {} }),
22}));
23
24import { render, stubGlobal } from "../../../test/render";
25import { ChatHeader } from "./ChatHeader";
26
27function header(title: string) {
28 return (
29 <>
30 <ChatHeader
31 activeThreadEnvironmentId={EnvironmentId.make("environment")}
32 activeThreadId={ThreadId.make("thread")}
33 activeThreadTitle={title}
34 isServerThread
35 activeProject={null}
36 parentThreadLink={null}
37 onOpenThread={() => {}}
38 rightPanelOpen={false}
39 onNewThreadInProject={() => {}}
40 />
41 <div tabIndex={0}>Outside rename</div>
42 </>
43 );
44}
45
46it.each(["blur", "Enter"] as const)(
47 "preserves a concurrent title update when an untouched rename ends with %s",
48 async (finish) => {
49 persisted.title = "New thread";
50 await using screen = await render(header(persisted.title));
51 await screen.getByRole("button", { name: "Thread actions for New thread" }).dblClick();
52 const input = screen.getByRole("textbox", { name: "Thread title" });
53 await expect.element(input).toHaveValue("New thread");
54
55 persisted.title = "Fix login redirect";
56 await screen.rerender(header(persisted.title));
57 if (finish === "Enter") await userEvent.keyboard("{Enter}");
58 else await screen.getByText("Outside rename").click();
59
60 await expect.element(input).not.toBeInTheDocument();
61 expect(persisted.title).toBe("Fix login redirect");
62
63 await screen.rerender(header(persisted.title));
64 await screen.getByRole("button", { name: "Thread actions for Fix login redirect" }).dblClick();
65 await screen.getByRole("textbox", { name: "Thread title" }).fill("My edited title");
66 persisted.title = "Another concurrent title";
67 await screen.rerender(header(persisted.title));
68 await userEvent.keyboard("{Enter}");
69 await expect
70 .element(screen.getByRole("textbox", { name: "Thread title" }))
71 .not.toBeInTheDocument();
72 expect(persisted.title).toBe("My edited title");
73 },
74);
75
76it("opens the thread menu on a desktop title click without waiting", async () => {
77 openMenu.mockClear();
78 using _bridge = stubGlobal("desktopBridge", {});
79 await using screen = await render(header("New thread"));
80 await screen.getByRole("button", { name: "Thread actions for New thread" }).click();
81 expect(openMenu).toHaveBeenCalledOnce();
82});