1import {
2 createMemoryHistory,
3 createRootRoute,
4 createRoute,
5 createRouter,
6 Outlet,
7 RouterProvider,
8} from "@tanstack/react-router";
9import { describe, expect, it } from "vite-plus/test";
10
11import { act, render } from "../../test/render";
12import { startMainAppLocationTracking } from "../components/sidebar/mainAppLocation";
13import { useShownThreadRouteTarget } from "./useShownThreadRouteTarget";
14
15function ShownThread() {
16 const { shownTarget } = useShownThreadRouteTarget();
17 const label =
18 shownTarget === null
19 ? "none"
20 : shownTarget.kind === "server"
21 ? shownTarget.threadRef.threadId
22 : shownTarget.draftId;
23 return (
24 <>
25 <output>{label}</output>
26 <Outlet />
27 </>
28 );
29}
30
31function createAppRouter(initialPath: string) {
32 const rootRoute = createRootRoute({ component: ShownThread });
33 const threadRoute = createRoute({
34 getParentRoute: () => rootRoute,
35 path: "$environmentId/$threadId",
36 });
37 const settingsRoute = createRoute({ getParentRoute: () => rootRoute, path: "settings" });
38 const settingsGeneralRoute = createRoute({
39 getParentRoute: () => settingsRoute,
40 path: "general",
41 });
42 const usageRoute = createRoute({ getParentRoute: () => rootRoute, path: "usage" });
43 const newRoute = createRoute({ getParentRoute: () => rootRoute, path: "new" });
44 return createRouter({
45 routeTree: rootRoute.addChildren([
46 threadRoute,
47 settingsRoute.addChildren([settingsGeneralRoute]),
48 usageRoute,
49 newRoute,
50 ]),
51 history: createMemoryHistory({ initialEntries: [initialPath] }),
52 });
53}
54
55describe("useShownThreadRouteTarget", () => {
56 it("keeps the thread that opened Settings or Usage shown until the user leaves for another page", async () => {
57 const router = createAppRouter("/env-1/thread-a");
58 await router.load();
59 // The hook reads thread params and modal paths from real routes, which the default router lacks.
60 await using screen = await render(<RouterProvider router={router} />, { router: false });
61 const shown = screen.getByRole("status");
62 await expect.element(shown).toHaveTextContent("thread-a");
63
64 await act(() => router.navigate({ to: "/settings/general" }));
65 await expect.element(shown).toHaveTextContent("thread-a");
66
67 await act(() => router.navigate({ to: "/usage" }));
68 await expect.element(shown).toHaveTextContent("thread-a");
69
70 await act(() => router.navigate({ href: "/env-1/thread-b" }));
71 await expect.element(shown).toHaveTextContent("thread-b");
72
73 await act(() => router.navigate({ to: "/new" }));
74 await expect.element(shown).toHaveTextContent("none");
75
76 await act(() => router.navigate({ to: "/settings/general" }));
77 await expect.element(shown).toHaveTextContent("none");
78 });
79
80 it("shows the thread behind Settings when it mounts while Settings is already open", async () => {
81 const router = createAppRouter("/env-1/thread-a");
82 await router.load();
83 using _tracking = { [Symbol.dispose]: startMainAppLocationTracking(router) };
84 await router.navigate({ to: "/settings/general" });
85
86 // The hook reads thread params and modal paths from real routes, which the default router lacks.
87 await using screen = await render(<RouterProvider router={router} />, { router: false });
88
89 await expect.element(screen.getByRole("status")).toHaveTextContent("thread-a");
90 });
91});