diff --git a/apps/web/src/components/settings/SettingsBreadcrumb.test.tsx b/apps/web/src/components/settings/SettingsBreadcrumb.test.tsx new file mode 100644 index 000000000000..b23200c5559b --- /dev/null +++ b/apps/web/src/components/settings/SettingsBreadcrumb.test.tsx @@ -0,0 +1,130 @@ +import { + Outlet, + RouterContextProvider, + createMemoryHistory, + createRootRoute, + createRoute, + createRouter, + useLocation, +} from "@tanstack/react-router"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it, vi } from "vite-plus/test"; + +import { SettingsBreadcrumb } from "./SettingsBreadcrumb"; + +vi.mock("../../lib/archivedThreadsState", () => ({ + useArchivedProjectModel: () => ({ + isLoading: false, + projectGroups: [{ projectKey: "project-alpha", displayName: "Alpha Project" }], + }), +})); + +function createArchiveRouter(initialEntry: string, pauseArchive: boolean) { + let markArchiveStarted = () => {}; + let releaseArchive = () => {}; + const archiveStarted = new Promise((resolve) => { + markArchiveStarted = resolve; + }); + const archiveReleased = new Promise((resolve) => { + releaseArchive = resolve; + }); + + const rootRoute = createRootRoute({ + component: Outlet, + }); + const settingsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "settings", + component: SettingsTestLayout, + }); + const generalRoute = createRoute({ + getParentRoute: () => settingsRoute, + path: "general", + component: () =>
General settings
, + }); + const archivedRoute = createRoute({ + getParentRoute: () => settingsRoute, + path: "archived", + validateSearch: (search): { project?: string } => + typeof search.project === "string" ? { project: search.project } : {}, + beforeLoad: async () => { + if (!pauseArchive) return; + markArchiveStarted(); + await archiveReleased; + }, + component: () =>
Archived threads
, + }); + const routeTree = rootRoute.addChildren([ + settingsRoute.addChildren([generalRoute, archivedRoute]), + ]); + const router = createRouter({ + routeTree, + history: createMemoryHistory({ initialEntries: [initialEntry] }), + }); + + return { archiveStarted, releaseArchive, router }; +} + +function SettingsTestLayout() { + const pathname = useLocation({ select: (location) => location.pathname }); + return ( + <> + + + + ); +} + +function renderBreadcrumb(router: ReturnType["router"]) { + return renderToStaticMarkup( + + + , + ); +} + +describe("SettingsBreadcrumb", () => { + it("waits for the Archive match before reading its search during navigation", async () => { + const { archiveStarted, releaseArchive, router } = createArchiveRouter( + "/settings/general", + true, + ); + await router.load(); + + const navigation = router.navigate({ + to: "/settings/archived", + search: { project: "project-alpha" }, + }); + await archiveStarted; + + expect(router.state.location.pathname).toBe("/settings/archived"); + expect(router.state.matches.some((match) => match.routeId === "/settings/archived")).toBe( + false, + ); + const pendingMarkup = renderBreadcrumb(router); + expect(pendingMarkup).toContain('aria-label="Settings breadcrumb"'); + expect(pendingMarkup).toContain("Settings"); + expect(pendingMarkup).toContain("Archive"); + + releaseArchive(); + await navigation; + + const archiveMarkup = renderBreadcrumb(router); + expect(archiveMarkup).toContain("Archive"); + expect(archiveMarkup).toContain("Alpha Project"); + + await router.navigate({ to: "/settings/general" }); + const generalMarkup = renderBreadcrumb(router); + expect(generalMarkup).toContain("General"); + expect(generalMarkup).not.toContain("Alpha Project"); + }); + + it("preserves the selected project on a direct Archive deep link", async () => { + const { router } = createArchiveRouter("/settings/archived?project=project-alpha", false); + await router.load(); + + const markup = renderBreadcrumb(router); + expect(router.state.location.search.project).toBe("project-alpha"); + expect(markup).toContain("Alpha Project"); + }); +}); diff --git a/apps/web/src/components/settings/SettingsBreadcrumb.tsx b/apps/web/src/components/settings/SettingsBreadcrumb.tsx index 67223955cc96..7217989d049d 100644 --- a/apps/web/src/components/settings/SettingsBreadcrumb.tsx +++ b/apps/web/src/components/settings/SettingsBreadcrumb.tsx @@ -20,9 +20,14 @@ function settingsBreadcrumbLabel(pathname: string): string | null { } export function SettingsBreadcrumb({ pathname }: { pathname: string }) { + const archiveProjectKey = useSearch({ + from: "/settings/archived", + shouldThrow: false, + select: (search) => search.project ?? null, + }); const normalizedPathname = pathname.replace(/\/+$/, "") || "/"; - if (normalizedPathname === "/settings/archived") { - return ; + if (normalizedPathname === "/settings/archived" && archiveProjectKey !== undefined) { + return ; } const sectionLabel = settingsBreadcrumbLabel(pathname); @@ -41,8 +46,7 @@ export function SettingsBreadcrumb({ pathname }: { pathname: string }) { ); } -function ArchivedThreadsBreadcrumb() { - const search = useSearch({ from: "/settings/archived" }); +function ArchivedThreadsBreadcrumb({ projectKey }: { projectKey: string | null }) { const navigate = useNavigate({ from: "/settings/archived" }); const { isLoading, projectGroups } = useArchivedProjectModel(); @@ -59,7 +63,7 @@ function ArchivedThreadsBreadcrumb() { }); }} rootLabel="Archive" - selectedKey={search.project ?? null} + selectedKey={projectKey} unavailableLabel={isLoading ? "Loading project" : "Unavailable project"} /> );