diff --git a/apps/web/src/components/settings/SettingsScopeSentence.tsx b/apps/web/src/components/settings/SettingsScopeSentence.tsx index 4223ebc90775fbe09bdb29fa1e8ab4930ffff56a..04312fd031427199b3bf5aa7aae3551d27eb297d 100644 --- a/apps/web/src/components/settings/SettingsScopeSentence.tsx +++ b/apps/web/src/components/settings/SettingsScopeSentence.tsx @@ -152,7 +152,7 @@ function EnvironmentScopeMenu({ > {!singleEnvironment ? ( <> - + All environments @@ -163,7 +163,11 @@ function EnvironmentScopeMenu({ ) : null} {environments.map((environment) => ( - + - + All projects @@ -207,7 +211,7 @@ function ProjectScopeMenu({ value, groups, onChange }: SettingsScopeMenuProps) { {groups.map((group) => ( - + {group.displayName} diff --git a/apps/web/src/components/settings/settingsScopeRetention.test.tsx b/apps/web/src/components/settings/settingsScopeRetention.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a44d73e0fdae9f2c1c1045b347d72b21d5fdd942 --- /dev/null +++ b/apps/web/src/components/settings/settingsScopeRetention.test.tsx @@ -0,0 +1,128 @@ +// @vitest-environment jsdom +import { act, useState, type ReactNode } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; +import type { SettingsScopeSearch } from "./settingsScope"; + +interface ScopeState { + readonly search: SettingsScopeSearch; + readonly environmentId: string | null; +} + +const state = vi.hoisted(() => ({ + pathname: "/settings/diagnostics", + scope: { search: {}, environmentId: null } as ScopeState, +})); + +vi.mock("@tanstack/react-router", async (importOriginal) => ({ + ...(await importOriginal()), + createFileRoute: () => (options: { component: () => ReactNode }) => ({ + options, + useSearch: () => state.scope.search, + useNavigate: () => vi.fn(), + }), + useLocation: (options?: { select: (location: unknown) => unknown }) => { + const location = { pathname: state.pathname, hash: "", state: {} }; + return options ? options.select(location) : location; + }, + Outlet: () => , + redirect: vi.fn(), +})); +vi.mock("./SettingsScopeContext", async () => { + const { createContext, useContext } = await import("react"); + const Scope = createContext(state.scope); + return { + SettingsScopeProvider: ({ children }: { children: ReactNode }) => ( + {children} + ), + useSettingsScope: () => { + const { search, environmentId } = useContext(Scope); + return { + search, + scope: { kind: "all" }, + environment: environmentId ? { environmentId } : null, + connectedEnvironments: environmentId ? [{}] : [], + }; + }, + }; +}); +vi.mock("../../state/environments", () => ({ useEnvironments: () => ({ environments: [] }) })); +vi.mock("../ui/sidebar", () => ({ + SidebarInset: ({ children }: { children: ReactNode }) =>
{children}
, +})); +vi.mock("../WorkspacePageHeader", () => ({ + WorkspacePageHeader: ({ children }: { children: ReactNode }) =>
{children}
, +})); +vi.mock("./settingsLayout", () => ({ + SettingsPageContainer: ({ children }: { children: ReactNode }) =>
{children}
, +})); +vi.mock("./SettingsBreadcrumb", () => ({ SettingsBreadcrumb: () => null })); +vi.mock("../sidebar/mainAppLocation", () => ({ useNavigateToMainApp: () => vi.fn() })); +vi.mock("../../hooks/useNavigateBack", () => ({ useEscapeToGoBack: () => undefined })); +vi.mock("./SettingsPanels", () => ({ useSettingsRestore: () => ({ changedSettingLabels: [] }) })); + +import { Route } from "../../routes/settings"; + +function StatefulContent() { + const [value, setValue] = useState(0); + return ( + + ); +} + +let root: Root; +let container: HTMLDivElement; +const Layout = Route.options.component!; +await Layout.preload?.(); + +async function render(scope: ScopeState) { + state.scope = scope; + await act(async () => root.render()); +} +function retainedValue() { + return container.querySelector('button[aria-label="Retained value"]')?.textContent ?? null; +} + +beforeEach(() => { + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); +}); +afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); + vi.unstubAllGlobals(); +}); + +const first = { search: { machine: "first", project: "one" }, environmentId: "first" }; +const otherProject = { search: { machine: "first", project: "two" }, environmentId: "first" }; +const otherMachine = { search: { machine: "second", project: "one" }, environmentId: "second" }; +const implicitFirst = { search: { project: "one" }, environmentId: "first" }; +const implicitSecond = { search: { project: "one" }, environmentId: "second" }; + +const disconnected = { search: first.search, environmentId: null }; + +it.each([ + ["/settings/diagnostics", "another project", "1", first, otherProject], + ["/settings/providers", "another project", "1", first, otherProject], + ["/settings/diagnostics", "another environment", "0", first, otherMachine], + ["/settings/providers", "another environment", "0", first, otherMachine], + ["/settings/diagnostics", "an implicit environment change", "0", implicitFirst, implicitSecond], + ["/settings/providers", "an implicit environment change", "0", implicitFirst, implicitSecond], + ["/settings/diagnostics", "a disconnect", "0", first, disconnected], + ["/settings/keybindings", "another project", "0", first, otherProject], +])("%s after %s shows %s", async (pathname, _change, expected, before, after) => { + state.pathname = pathname; + await render(before); + await act(async () => + container.querySelector('button[aria-label="Retained value"]')?.click(), + ); + expect(retainedValue()).toBe("1"); + + await render(after); + + expect(retainedValue()).toBe(expected); +}); diff --git a/apps/web/src/routes/settings.tsx b/apps/web/src/routes/settings.tsx index 576307fbe7addbc23c78b6059639221549525e26..a1e02c12d929293990c96d946218b8c6ff615c65 100644 --- a/apps/web/src/routes/settings.tsx +++ b/apps/web/src/routes/settings.tsx @@ -114,7 +114,11 @@ function SettingsContentLayout() { const location = useLocation(); const navigateToMainApp = useNavigateToMainApp(); useEscapeToGoBack(navigateToMainApp); - const { search } = useSettingsScope(); + const { search, environment } = useSettingsScope(); + const contentScopeKey = + location.pathname === "/settings/diagnostics" || location.pathname === "/settings/providers" + ? environment?.environmentId + : JSON.stringify(search); const [restoreSignal, setRestoreSignal] = useState(0); return ( @@ -133,10 +137,7 @@ function SettingsContentLayout() { -
+