Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions apps/web/src/components/settings/SettingsBreadcrumb.test.tsx
Original file line number Diff line number Diff line change
@@ -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<void>((resolve) => {
markArchiveStarted = resolve;
});
const archiveReleased = new Promise<void>((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: () => <div>General settings</div>,
});
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: () => <div>Archived threads</div>,
});
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 (
<>
<SettingsBreadcrumb pathname={pathname} />
<Outlet />
</>
);
}

function renderBreadcrumb(router: ReturnType<typeof createArchiveRouter>["router"]) {
return renderToStaticMarkup(
<RouterContextProvider router={router}>
<SettingsBreadcrumb pathname={router.state.location.pathname} />
</RouterContextProvider>,
);
}

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");
});
});
14 changes: 9 additions & 5 deletions apps/web/src/components/settings/SettingsBreadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ArchivedThreadsBreadcrumb />;
if (normalizedPathname === "/settings/archived" && archiveProjectKey !== undefined) {
return <ArchivedThreadsBreadcrumb projectKey={archiveProjectKey} />;
}
const sectionLabel = settingsBreadcrumbLabel(pathname);

Expand All @@ -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();

Expand All @@ -59,7 +63,7 @@ function ArchivedThreadsBreadcrumb() {
});
}}
rootLabel="Archive"
selectedKey={search.project ?? null}
selectedKey={projectKey}
unavailableLabel={isLoading ? "Loading project" : "Unavailable project"}
/>
);
Expand Down