Files
kandev/apps/web/components/settings/settings-layout-client.test.tsx
T

216 lines
6.3 KiB
TypeScript

import { cleanup, render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let pathname = "/settings/integrations/github";
const COPY_CONFIG_TEST_ID = "mock-copy-config";
const state = {
configChat: { isOpen: false },
workspaces: {
activeId: "ws-1",
items: [
{ id: "ws-1", name: "Default" },
{ id: "ws-2", name: "Archive" },
],
},
setActiveWorkspace: vi.fn(),
};
vi.mock("@/lib/routing/client-router", () => ({
usePathname: () => pathname,
useRouter: () => ({ replace: vi.fn() }),
useSearchParams: () => new URLSearchParams(),
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (s: typeof state) => unknown) => selector(state),
useOptionalAppStore: (selector: (s: typeof state) => unknown, fallback: unknown) =>
selector(state) ?? fallback,
}));
vi.mock("@/components/page-topbar", () => ({
PageTopbar: ({
actions,
parents,
title,
}: {
actions?: ReactNode;
parents?: Array<{ label: string; href: string }>;
title: string;
}) => (
<div>
<nav data-testid="page-topbar-breadcrumbs">
{parents?.map((parent) => (
<a key={parent.href} href={parent.href}>
{parent.label}
</a>
))}
<span data-testid="page-topbar-title">{title}</span>
</nav>
<div data-testid="page-topbar-actions">{actions}</div>
</div>
),
}));
vi.mock("@kandev/ui/tooltip", () => ({
TooltipProvider: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("@/components/integrations/integration-copy-config-menu", () => ({
IntegrationCopyConfigMenu: ({ sourceWorkspaceId }: { sourceWorkspaceId: string }) => (
<div data-testid={COPY_CONFIG_TEST_ID} data-source-workspace-id={sourceWorkspaceId} />
),
}));
import { SettingsLayoutClient } from "./settings-layout-client";
import { useSettingsSaveContributor } from "./settings-save-provider";
import { i18n } from "@/lib/i18n";
function DirtySettings() {
useSettingsSaveContributor({
id: "dirty-settings",
revision: 1,
isDirty: true,
save: vi.fn(),
discard: vi.fn(),
});
return <div>Dirty settings</div>;
}
describe("SettingsLayoutClient", () => {
beforeEach(() => {
pathname = "/settings/integrations/github";
state.workspaces.activeId = "ws-1";
state.setActiveWorkspace.mockClear();
});
afterEach(() => cleanup());
it("keeps copy config available without rendering the workspace switcher", () => {
render(
<SettingsLayoutClient>
<div>Settings page</div>
</SettingsLayoutClient>,
);
expect(screen.queryByTestId("integration-workspace-switcher")).toBeNull();
expect(screen.getByTestId(COPY_CONFIG_TEST_ID).dataset.sourceWorkspaceId).toBe("ws-1");
});
it("shows copy config on workspace-scoped integration pages", () => {
pathname = "/settings/workspace/ws-1/integrations/github";
render(
<SettingsLayoutClient>
<div>Settings page</div>
</SettingsLayoutClient>,
);
expect(screen.getByTestId(COPY_CONFIG_TEST_ID).dataset.sourceWorkspaceId).toBe("ws-1");
});
it("uses the workspace from scoped integration routes before store hydration catches up", () => {
pathname = "/settings/workspace/ws-2/integrations/github";
state.workspaces.activeId = "ws-1";
render(
<SettingsLayoutClient>
<div>Settings page</div>
</SettingsLayoutClient>,
);
expect(screen.getByTestId(COPY_CONFIG_TEST_ID).dataset.sourceWorkspaceId).toBe("ws-2");
});
it("falls back to the active workspace when a scoped route has invalid encoding", () => {
pathname = "/settings/workspace/%E0%A4%A/integrations/github";
state.workspaces.activeId = "ws-1";
render(
<SettingsLayoutClient>
<div>Settings page</div>
</SettingsLayoutClient>,
);
expect(screen.getByTestId(COPY_CONFIG_TEST_ID).dataset.sourceWorkspaceId).toBe("ws-1");
});
it("hosts the route save action and reserves safe-area scroll space", async () => {
pathname = "/settings/general/appearance";
render(
<SettingsLayoutClient>
<DirtySettings />
</SettingsLayoutClient>,
);
expect(await screen.findByTestId("settings-floating-save")).toBeTruthy();
expect(screen.getByTestId("settings-scroll-container").className).toContain(
"safe-area-inset-bottom",
);
expect(screen.getByTestId("settings-scroll-container").className).toContain(
"app-status-bar-height",
);
});
it("translates the Message Queue breadcrumb and keeps the shared scroll owner", async () => {
pathname = "/settings/general/message-queue";
await i18n.changeLanguage("pseudo");
try {
render(
<SettingsLayoutClient>
<div>Queue settings</div>
</SettingsLayoutClient>,
);
expect(screen.getByTestId("page-topbar-title").textContent).toBe("Ḿēśśàĝē Qũēũē");
expect(screen.getByTestId("settings-scroll-container").className).toContain(
"overflow-y-auto",
);
} finally {
await i18n.changeLanguage("en");
}
});
});
describe("SettingsLayoutClient workspace breadcrumbs", () => {
beforeEach(() => {
pathname = "/settings/workspace/ws-2/secrets";
state.workspaces.activeId = "ws-1";
state.setActiveWorkspace.mockClear();
});
afterEach(() => cleanup());
it("includes the workspace name in workspace-scoped breadcrumbs", () => {
render(
<SettingsLayoutClient>
<div>Settings page</div>
</SettingsLayoutClient>,
);
expect(screen.getByTestId("page-topbar-breadcrumbs").textContent).toBe(
"SettingsArchiveSecrets",
);
expect(screen.getByRole("link", { name: "Archive" }).getAttribute("href")).toBe(
"/settings/workspace/ws-2",
);
});
it("keeps the automations parent after the workspace breadcrumb", () => {
pathname = "/settings/workspace/ws-2/automations/new";
render(
<SettingsLayoutClient>
<div>Settings page</div>
</SettingsLayoutClient>,
);
const breadcrumbs = screen.getByTestId("page-topbar-breadcrumbs");
expect(
Array.from(breadcrumbs.querySelectorAll("a, span")).map((element) => element.textContent),
).toEqual(["Settings", "Archive", "Automations", "New"]);
});
});