import { render, screen, within } from "@testing-library/react"; import { createRoutesStub } from "react-router"; import { describe, expect, it, vi } from "vitest"; import { QueryClientProvider } from "@tanstack/react-query"; import SettingsScreen, { clientLoader } from "#/routes/settings"; import OptionService from "#/api/option-service/option-service.api"; // Mock the i18next hook vi.mock("react-i18next", async () => { const actual = await vi.importActual("react-i18next"); return { ...actual, useTranslation: () => ({ t: (key: string) => { const translations: Record = { SETTINGS$NAV_INTEGRATIONS: "Integrations", SETTINGS$NAV_APPLICATION: "Application", SETTINGS$NAV_CREDITS: "Credits", SETTINGS$NAV_API_KEYS: "API Keys", SETTINGS$NAV_LLM: "LLM", SETTINGS$NAV_SECRETS: "Secrets", SETTINGS$NAV_MCP: "MCP", SETTINGS$NAV_USER: "User", SETTINGS$TITLE: "Settings", }; return translations[key] || key; }, i18n: { changeLanguage: vi.fn(), }, }), }; }); describe("Settings Screen", () => { const { handleLogoutMock, mockQueryClient } = vi.hoisted(() => ({ handleLogoutMock: vi.fn(), mockQueryClient: (() => { const { QueryClient } = require("@tanstack/react-query"); return new QueryClient(); })(), })); vi.mock("#/hooks/use-app-logout", () => ({ useAppLogout: vi.fn().mockReturnValue({ handleLogout: handleLogoutMock }), })); vi.mock("#/query-client-config", () => ({ queryClient: mockQueryClient, })); const RouterStub = createRoutesStub([ { Component: SettingsScreen, // @ts-expect-error - custom loader clientLoader, path: "/settings", children: [ { Component: () =>
, path: "/settings", }, { Component: () =>
, path: "/settings/integrations", }, { Component: () =>
, path: "/settings/app", }, { Component: () =>
, path: "/settings/billing", }, { Component: () =>
, path: "/settings/api-keys", }, ], }, ]); const renderSettingsScreen = (path = "/settings") => render(, { wrapper: ({ children }) => ( {children} ), }); it("should render the navbar", async () => { const sectionsToInclude = ["llm", "integrations", "application", "secrets"]; const sectionsToExclude = ["api keys", "credits", "billing"]; const getConfigSpy = vi.spyOn(OptionService, "getConfig"); // @ts-expect-error - only return app mode getConfigSpy.mockResolvedValue({ APP_MODE: "oss", }); // Clear any existing query data mockQueryClient.clear(); renderSettingsScreen(); const navbar = await screen.findByTestId("settings-navbar"); sectionsToInclude.forEach((section) => { const sectionElement = within(navbar).getByText(section, { exact: false, // case insensitive }); expect(sectionElement).toBeInTheDocument(); }); sectionsToExclude.forEach((section) => { const sectionElement = within(navbar).queryByText(section, { exact: false, // case insensitive }); expect(sectionElement).not.toBeInTheDocument(); }); getConfigSpy.mockRestore(); }); it("should render the saas navbar", async () => { const saasConfig = { APP_MODE: "saas" }; // Clear any existing query data and set the config mockQueryClient.clear(); mockQueryClient.setQueryData(["config"], saasConfig); const sectionsToInclude = [ "llm", // LLM settings are now always shown in SaaS mode "user", "integrations", "application", "billing", // The nav item shows "Billing" text and routes to /billing "secrets", "api keys", ]; const sectionsToExclude: string[] = []; // No sections are excluded in SaaS mode now renderSettingsScreen(); const navbar = await screen.findByTestId("settings-navbar"); sectionsToInclude.forEach((section) => { const sectionElement = within(navbar).getByText(section, { exact: false, // case insensitive }); expect(sectionElement).toBeInTheDocument(); }); sectionsToExclude.forEach((section) => { const sectionElement = within(navbar).queryByText(section, { exact: false, // case insensitive }); expect(sectionElement).not.toBeInTheDocument(); }); }); it("should not be able to access saas-only routes in oss mode", async () => { const getConfigSpy = vi.spyOn(OptionService, "getConfig"); // @ts-expect-error - only return app mode getConfigSpy.mockResolvedValue({ APP_MODE: "oss", }); // Clear any existing query data mockQueryClient.clear(); // In OSS mode, accessing restricted routes should redirect to /settings // Since createRoutesStub doesn't handle clientLoader redirects properly, // we test that the correct navbar is shown (OSS navbar) and that // the restricted route components are not rendered when accessing /settings renderSettingsScreen("/settings"); // Verify we're in OSS mode by checking the navbar const navbar = await screen.findByTestId("settings-navbar"); expect(within(navbar).getByText("LLM")).toBeInTheDocument(); expect( within(navbar).queryByText("credits", { exact: false }), ).not.toBeInTheDocument(); // Verify the LLM settings screen is shown expect(screen.getByTestId("llm-settings-screen")).toBeInTheDocument(); expect( screen.queryByTestId("billing-settings-screen"), ).not.toBeInTheDocument(); expect( screen.queryByTestId("api-keys-settings-screen"), ).not.toBeInTheDocument(); getConfigSpy.mockRestore(); }); it.todo("should not be able to access oss-only routes in saas mode"); });