diff --git a/frontend/src/pages/.pi-map.index.md b/frontend/src/pages/.pi-map.index.md
index 127a8aa..2845161 100644
--- a/frontend/src/pages/.pi-map.index.md
+++ b/frontend/src/pages/.pi-map.index.md
@@ -2,7 +2,7 @@
dir: frontend/src/pages
## role
-Top-level page components that render the main UI views for dashboards, services, and settings in the application.
+Top-level page components that render the application's primary UI views including dashboards, service management, and settings.
## parent
index: frontend/src/.pi-map.index.md
map: frontend/src/.pi-map.md
diff --git a/frontend/src/pages/.pi-map.md b/frontend/src/pages/.pi-map.md
index 24dd913..88ed91a 100644
--- a/frontend/src/pages/.pi-map.md
+++ b/frontend/src/pages/.pi-map.md
@@ -4,18 +4,18 @@ dir: frontend/src/pages
index: frontend/src/pages/.pi-map.index.md
## role
-Top-level page components that render the main UI views for dashboards, services, and settings in the application.
+Top-level page components that render the application's primary UI views including dashboards, service management, and settings.
## files
- Dashboard.tsx | This file renders the main dashboard page, displaying widget instances grouped by category and user-created shortcuts with full CRUD dialog management. | exp: func:Dashboard(), call:useNavigate, call:useDashboardShortcuts, call:useSaveDashboardShortcut, call:useDeleteDashboardShortcut, call:useState, call:emptyShortcut, call:useWidgetInstances, call:useWidgetReferences, call:useServiceInstances, call:useIsMobile, call:useMemo, call:widgetReferences .filter((r) => r.widget.enabled) .map, call:[...widgetInstances, ...refs] .filter((w) => w.enabled) .sort, call:widgetReferences.map, call:useDetachWidgetReference, call:groupWidgetsBySection, call:setShortcutDraft, call:setShortcutDialogOpen, call:saveShortcut.mutateAsync, call:navigate, call:setWidgetDialogOpen, call:shortcuts.map, call:shortcutHref, call:window.open, call:openEditShortcut, call:setDeleteShortcutId, call:setEditWidgetId, call:widgetReferences.find, call:detachRef.mutate, call:visibleWidgets.map, call:referencedWidgetIds.has, call:Boolean, call:deleteShortcut.mutate | dep: react, react-router-dom, lucide-react, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/switch, ../hooks/useDashboard, ../hooks/useWidgets, ../hooks/useServices, ../hooks/useIsMobile, ../types, ../components/SectionCard, ../components/ConfirmDialog, ../components/DialogFooter, ../components/WidgetInstance, ../components/WidgetConfigDialog, @/components/ui/*
- NamedDashboardPage.tsx | Renders a named dashboard page that displays pinned service links and configurable widgets based on a slug-based lookup. | exp: func:NamedDashboardPage(), call:useParams, call:useDashboardBySlug, call:useWidgetReferences, call:useState, call:useMemo, call:parseItems, call:widgetRefs .filter((r) => r.widget.enabled) .map((r) => r.widget) .sort, call:setConfigOpen, call:visibleWidgets.map, call:setEditWidgetId, call:items.map | dep: react, react-router-dom, lucide-react, @/components/ui/alert, @/components/ui/button, @/components/ui/skeleton, ../hooks/useDashboards, ../hooks/useWidgets, ../components/PinnedServiceLink, ../components/WidgetInstance, ../components/WidgetConfigDialog, useDashboards hook, useWidgets hook, PinnedServiceLink, WidgetInstanceCard, WidgetConfigDialog
-- ServicePage.tsx | Displays a detailed service instance page with tabbed content, instance switching, and widget information based on URL parameters. | exp: func:ServicePage(), call:useParams, call:useServiceInstances, call:useNavigate, call:useMemo, call:services.find, call:getServiceBinding, call:serviceContentTabs, call:services.filter, call:binding.widgets.map, call:siblings.map, call:navigate, call:contentTabs.map, call:allTabs.map | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/tabs, ../hooks/useServices, ../types, ../components/SectionCard, ../integrations/registry, ./service-tabs
+- ServicePage.tsx | Displays a detailed dashboard page for a specific service instance with overview, content tabs, and widgets. | exp: func:ServicePage(), call:useParams, call:useServiceInstances, call:useNavigate, call:useMemo, call:services.find, call:getServiceBinding, call:serviceContentTabs, call:services.filter, call:binding.widgets.map, call:navigate, call:encodeURIComponent, call:siblings.map, call:contentTabs.map, call:allTabs.map | dep: react, react-router-dom, lucide-react, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/tabs, ../hooks/useServices, ../types, ../components/SectionCard, ../integrations/registry, ./service-tabs, @/components/ui, @/components/SectionCard
- ServiceTypePage.tsx | Resolves the first enabled service instance for a given type and redirects to it, or shows an empty state if none are configured. | exp: func:ServiceTypePage(), call:useParams, call:useServiceInstances, call:useMemo, call:instances.find | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/button, ../hooks/useServices, useServices hook
-- ServicesPage.tsx | Provides the UI and logic for configuring external service instances and managing named dashboards with pinned links. | exp: func:ServicesPage(), call:useNavigate, call:useServiceInstances, call:useServiceTypes, call:useDeleteServiceInstance, call:useState, call:useMemo, call:map.get, call:list.push, call:map.set, call:[...map.entries()].sort, call:map.entries, call:a[0].localeCompare, call:types.find, call:getServiceBinding, call:setCreateOpen, call:grouped.map, call:typeName, call:instances.map, call:Object.entries(s.secrets_set).some, call:navigate, call:setDeleteId, call:Boolean, call:deleteService.mutate | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/input, @/components/ui/label, @/components/ui/switch, @/components/ui/dialog, @/components/ui/select, lucide-react, ../hooks/useServices, ../hooks/useDashboards, ../types, ../components/SectionCard, ../components/ConfirmDialog, ../components/DialogFooter, ../integrations/registry, ../components/PinnedServiceLink, ../api/dashboards, @/components/ui/*, useServices, useDashboards, SectionCard, ConfirmDialog, DialogFooter, integrations/registry
-- Settings.tsx | Provides the settings page UI for managing monitoring machines, SSH keys, services, and database resets, including CRUD operations and SSH connection validation. | exp: func:Settings(), call:useMonitoringSettings, call:useSSHKeys, call:useSaveMonitoringMachine, call:useDeleteMonitoringMachine, call:useTestMonitoringMachineSSH, call:useState, call:emptyMachine, call:useIsMobile, call:useMemo, call:orderedMachines.find, call:setSSHValidationMessage, call:setSSHValidationError, call:setSSHValidationStatus, call:clearSSHValidation, call:setMachineDraft, call:setEditingMachine, call:setMachineDialogOpen, call:saveMachine.mutateAsync, call:testMachineSSH.mutateAsync, call:String, call:message.toLowerCase, call:lowered.includes, call:setTab, call:orderedMachines.map, call:setSelectedMachineId, call:cn, call:openEditMachine, call:setDeleteMachineId, call:closeMachineDialog, call:saveMachineDraft, call:machineDraft.host.trim, call:isMachineDraftDirty, call:Boolean, call:deleteMachine.mutate | dep: react, ../types, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../components/DialogFooter, ../components/HoverEditButton, ../components/SectionCard, ../components/SelectionRailCard, ../components/TabbedCard, ../components/ConfirmDialog, @/lib/utils, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/checkbox, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/switch, @/components/ui/tabs, @/components/ui/textarea, ../hooks/useServices, @/components/ui/*, @/hooks/useSettings, @/hooks/useServices, @/hooks/useIsMobile
+- ServicesPage.tsx | Renders the services management page, allowing users to create, configure, test, and delete service instances, as well as manage custom dashboards composed of pinned service links. | exp: func:ServicesPage(), call:useNavigate, call:useServiceInstances, call:useServiceTypes, call:useDeleteServiceInstance, call:useState, call:useMemo, call:map.get, call:list.push, call:map.set, call:[...map.entries()].sort, call:map.entries, call:a[0].localeCompare, call:types.find, call:getServiceBinding, call:setCreateOpen, call:grouped.map, call:typeName, call:instances.map, call:Object.entries(s.secrets_set).some, call:navigate, call:setDeleteId, call:Boolean, call:deleteService.mutate | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/input, @/components/ui/label, @/components/ui/textarea, @/components/ui/switch, @/components/ui/dialog, @/components/ui/select, lucide-react, ../hooks/useServices, ../hooks/useDashboards, ../types, ../components/SectionCard, ../components/ConfirmDialog, ../components/DialogFooter, ../integrations/registry, ../components/ServiceTestPanel, ../components/PinnedServiceLink, ../api/dashboards, @/components/ui/*, useServices, useDashboards, SectionCard, ConfirmDialog, DialogFooter, ServiceTestPanel, getServiceBinding, serviceLinkTarget
+- Settings.tsx | Provides a settings dashboard UI for managing monitoring machines, SSH keys, service instances, and danger zone operations with CRUD forms and validation. | exp: func:Settings(), call:useMonitoringSettings, call:useSSHKeys, call:useSaveMonitoringMachine, call:useDeleteMonitoringMachine, call:useTestMonitoringMachineSSH, call:useSearchParams, call:useState, call:searchParams.get, call:emptyMachine, call:useIsMobile, call:useMemo, call:orderedMachines.find, call:setSSHValidationMessage, call:setSSHValidationError, call:setSSHValidationStatus, call:clearSSHValidation, call:setMachineDraft, call:setEditingMachine, call:setMachineDialogOpen, call:saveMachine.mutateAsync, call:testMachineSSH.mutateAsync, call:String, call:message.toLowerCase, call:lowered.includes, call:setTab, call:orderedMachines.map, call:setSelectedMachineId, call:cn, call:openEditMachine, call:setDeleteMachineId, call:closeMachineDialog, call:saveMachineDraft, call:machineDraft.host.trim, call:isMachineDraftDirty, call:Boolean, call:deleteMachine.mutate | dep: react, react-router-dom, ../types, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../components/DialogFooter, ../components/HoverEditButton, ../components/SectionCard, ../components/SelectionRailCard, ../components/TabbedCard, ../components/ConfirmDialog, @/lib/utils, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/checkbox, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/switch, @/components/ui/tabs, @/components/ui/textarea, ../hooks/useServices, ../components/ServiceTestPanel, ../components (DialogFooter, SectionCard, SelectionRailCard, TabbedCard, ConfirmDialog, HoverEditButton, ServiceTestPanel), @/components/ui (multiple UI primitives)
## arch
-React functional components using URL-parameter-driven routing, tabbed interfaces, and dialog-based CRUD management with widget composition.
+Route-level React page components using composition of widgets, dialogs, and CRUD forms with slug/ID-based data resolution and conditional rendering for empty states.
## tags
-call:use, components, ui, call:set, service, machine, page, dashboard
+call:use, components, ui, call:set, service, machine, dashboard, page
## symbols
- Dashboard
- NamedDashboardPage
diff --git a/frontend/src/pages/ServicePage.tsx b/frontend/src/pages/ServicePage.tsx
index 9419f97..09b9106 100644
--- a/frontend/src/pages/ServicePage.tsx
+++ b/frontend/src/pages/ServicePage.tsx
@@ -1,7 +1,9 @@
import { useMemo } from "react";
import { useParams, useNavigate } from "react-router-dom";
+import { Settings as SettingsIcon } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useServiceInstances } from "../hooks/useServices";
import type { ServiceInstance } from "../types";
@@ -89,7 +91,21 @@ export function ServicePage() {
{instance.name}
{binding.description}
- {binding.name}
+
+ {binding.name}
+
+
{/* Instance tabs (only when >1 enabled sibling) */}
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx
index ef86231..5faedf1 100644
--- a/frontend/src/pages/Settings.tsx
+++ b/frontend/src/pages/Settings.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { useMemo, useState } from "react";
+import { useSearchParams } from "react-router-dom";
import type {
MonitoringMachine,
MonitoringMachineInput,
@@ -893,7 +894,11 @@ export function Settings() {
const saveMachine = useSaveMonitoringMachine();
const deleteMachine = useDeleteMonitoringMachine();
const testMachineSSH = useTestMonitoringMachineSSH();
- const [tab, setTab] = useState("machines");
+ const [searchParams] = useSearchParams();
+ const [tab, setTab] = useState(
+ (searchParams.get("tab") as SettingsTab | null) ?? "machines",
+ );
+ const initialServiceId = searchParams.get("service") ?? "";
const [deleteMachineId, setDeleteMachineId] = useState(null);
const [selectedSSHKeyId, setSelectedSSHKeyId] = useState("");
const [sshValidationMessage, setSSHValidationMessage] = useState("");
@@ -1195,7 +1200,9 @@ export function Settings() {
onSelectKeyId={setSelectedSSHKeyId}
/>
)}
- {tab === "services" && }
+ {tab === "services" && (
+
+ )}
{tab === "danger" && }
{isMobile ? (
@@ -1338,10 +1345,14 @@ export function Settings() {
* old ServicePage ConfigBody — the service page is now a pure operational
* view; all administration lives here.
*/
-function ServicesAdminCard() {
+function ServicesAdminCard({
+ initialServiceId = "",
+}: {
+ initialServiceId?: string;
+}) {
const { data: services = [] } = useServiceInstances();
const { data: types = [] } = useServiceTypes();
- const [selectedServiceId, setSelectedServiceId] = useState("");
+ const [selectedServiceId, setSelectedServiceId] = useState(initialServiceId);
const sortedServices = useMemo(
() =>
@@ -1416,6 +1427,11 @@ function ServicesAdminCard() {
>
{selectedService ? (
diff --git a/frontend/src/pages/__tests__/.pi-map.index.md b/frontend/src/pages/__tests__/.pi-map.index.md
index 770b5c8..659e484 100644
--- a/frontend/src/pages/__tests__/.pi-map.index.md
+++ b/frontend/src/pages/__tests__/.pi-map.index.md
@@ -2,7 +2,7 @@
dir: frontend/src/pages/__tests__
## role
-Test suite for top-level page components, ensuring correct rendering, user interactions, and state handling across Dashboard, NamedDashboard, Service, and Settings views.
+Test suite covering dashboard, service, and settings page components with mocked hooks and React Testing Library.
## parent
index: frontend/src/pages/.pi-map.index.md
map: frontend/src/pages/.pi-map.md
@@ -12,6 +12,7 @@ map: frontend/src/pages/.pi-map.md
- Dashboard.test.tsx
- NamedDashboardPage.test.tsx
- ServicePage.test.tsx
+- Settings.services.test.tsx
- Settings.test.tsx
## links
index: frontend/src/pages/__tests__/.pi-map.index.md
diff --git a/frontend/src/pages/__tests__/.pi-map.md b/frontend/src/pages/__tests__/.pi-map.md
index 4039b29..bffa699 100644
--- a/frontend/src/pages/__tests__/.pi-map.md
+++ b/frontend/src/pages/__tests__/.pi-map.md
@@ -4,16 +4,17 @@ dir: frontend/src/pages/__tests__
index: frontend/src/pages/__tests__/.pi-map.index.md
## role
-Test suite for top-level page components, ensuring correct rendering, user interactions, and state handling across Dashboard, NamedDashboard, Service, and Settings views.
+Test suite covering dashboard, service, and settings page components with mocked hooks and React Testing Library.
## files
- Dashboard.test.tsx | Tests the Dashboard component's shortcut CRUD functionality (creation, deletion, empty state rendering) while stubbing out composed widgets and data hooks. | dep: vitest, @testing-library/react, @testing-library/user-event, ../Dashboard, ../../types, react-router-dom, Dashboard, DashboardShortcut
- NamedDashboardPage.test.tsx | Tests the NamedDashboardPage component covering loading, error/404, data rendering, and empty states using mocked hooks. | dep: vitest, @testing-library/react, react-router-dom, @tanstack/react-query, ../NamedDashboardPage, ../../hooks/useDashboards, NamedDashboardPage, useDashboards, useWidgets, WidgetConfigDialog, WidgetInstance
- ServicePage.test.tsx | Tests the ServicePage component's tab rendering logic, verifying conditional display of tabs based on service type and instance count. | dep: vitest, @testing-library/react, @testing-library/user-event, react-router-dom, ../ServicePage, ../../types, ServicePage, ServiceInstance
-- Settings.test.tsx | Tests the Settings component's machine list rendering, editing, and deletion flows with mocked hooks. | dep: vitest, @testing-library/react, @testing-library/user-event, ../Settings, ../../types, Settings, MonitoringMachine type, useSettings hooks
+- Settings.services.test.tsx | Regression test verifying that the Settings component correctly resets editor state when switching services to prevent saving stale data from a previously selected service. | dep: vitest, @testing-library/react, @testing-library/user-event, react-router-dom, ../Settings, ../../hooks/useSettings, ../../hooks/useServices
+- Settings.test.tsx | Tests the Settings component's machine CRUD operations (render, edit/save, delete) using mocked hooks and React Testing Library. | dep: vitest, @testing-library/react, @testing-library/user-event, react-router-dom, ../Settings, ../../types, Settings, MonitoringMachine
## arch
-React Testing Library with Jest, using mocked hooks and stubbed child components to isolate page-level rendering logic and CRUD workflows.
+Standard React Testing Library pattern using mocked hooks/data sources, component rendering verification, and user interaction simulation across isolated test files per page component.
## tags
-react, @testing, library, dashboard, tests, component, rendering, hooks
+react, library, @testing, hooks, dashboard, component, vitest, router
## symbols
-
## workflows
diff --git a/frontend/src/pages/__tests__/Settings.services.test.tsx b/frontend/src/pages/__tests__/Settings.services.test.tsx
new file mode 100644
index 0000000..ffa3676
--- /dev/null
+++ b/frontend/src/pages/__tests__/Settings.services.test.tsx
@@ -0,0 +1,143 @@
+/**
+ * Regression test for the service-editor state-reset bug.
+ *
+ * ServiceConfigEditor initializes its editable state (name, config, secrets)
+ * from the `instance` prop via useState. The parent must remount it (via
+ * `key={instance.id}`) when the selected service changes — otherwise switching
+ * services in the rail keeps the PREVIOUS service's name/config in state while
+ * `instance.id`/`service_type` (read live from props) now point at the new one.
+ * A save then wrote the stale name/config onto the new service's row
+ * (e.g. saving qBittorrent renamed it "Jellyfin" with Jellyfin's URL).
+ */
+
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { MemoryRouter } from "react-router-dom";
+import { Settings } from "../Settings";
+
+const saveServiceMutate = vi.fn().mockResolvedValue({});
+
+// Settings invokes the machine/SSH hooks at the top level regardless of active
+// tab, so stub them out to keep the render focused on the Services editor.
+vi.mock("../../hooks/useSettings", () => ({
+ useMonitoringSettings: () => ({ data: [] }),
+ useSSHKeys: () => ({ data: [] }),
+ useSaveMonitoringMachine: () => ({ mutateAsync: vi.fn(), isPending: false }),
+ useDeleteMonitoringMachine: () => ({ mutate: vi.fn() }),
+ useTestMonitoringMachineSSH: () => ({
+ mutateAsync: vi.fn(),
+ isPending: false,
+ }),
+ useResetLocalDatabase: () => ({}),
+ useSaveSSHKey: () => ({ mutateAsync: vi.fn() }),
+ useGenerateSSHKey: () => ({ mutateAsync: vi.fn(), isPending: false }),
+ useDeleteSSHKey: () => ({ mutate: vi.fn() }),
+}));
+
+vi.mock("../../hooks/useServices", () => ({
+ useServiceTypes: () => ({
+ data: [
+ {
+ service_type: "jellyfin",
+ name: "Jellyfin",
+ description: "Media server",
+ config_schema: {
+ type: "object",
+ properties: {
+ base_url: { type: "string", description: "URL" },
+ timeout_seconds: { type: "integer" },
+ },
+ },
+ secret_fields: [{ key: "api_key", label: "API key", required: true }],
+ widget_kinds: [],
+ },
+ {
+ service_type: "qbittorrent",
+ name: "qBittorrent",
+ description: "Torrent client",
+ config_schema: {
+ type: "object",
+ properties: {
+ base_url: { type: "string" },
+ timeout_seconds: { type: "integer" },
+ },
+ },
+ secret_fields: [
+ { key: "username", label: "Username", required: true },
+ { key: "password", label: "Password", required: true },
+ ],
+ widget_kinds: [],
+ },
+ ],
+ }),
+ useServiceInstances: () => ({
+ data: [
+ {
+ id: "svc-jf",
+ service_type: "jellyfin",
+ name: "My Jellyfin",
+ config: { base_url: "https://jf.example.com", timeout_seconds: 60 },
+ secrets_set: { api_key: true },
+ enabled: true,
+ created_at: 0,
+ updated_at: 0,
+ },
+ {
+ id: "svc-qb",
+ service_type: "qbittorrent",
+ name: "My qBittorrent",
+ config: { base_url: "https://qb.example.com", timeout_seconds: 60 },
+ secrets_set: { username: true, password: true },
+ enabled: true,
+ created_at: 0,
+ updated_at: 0,
+ },
+ ],
+ }),
+ useSaveServiceInstance: () => ({
+ mutateAsync: saveServiceMutate,
+ isPending: false,
+ }),
+ useDeleteServiceInstance: () => ({ mutate: vi.fn() }),
+ useTestServiceInstance: () => ({
+ mutateAsync: vi.fn().mockResolvedValue({ ok: true }),
+ isPending: false,
+ }),
+}));
+
+beforeEach(() => {
+ saveServiceMutate.mockClear();
+});
+
+describe("Settings > Services editor", () => {
+ it("resets editable state when switching services so save does not clobber (regression)", async () => {
+ render(
+
+
+ ,
+ );
+
+ // Default selection is the first service (Jellyfin). Switch to qBittorrent
+ // via the selection rail.
+ await userEvent.click(screen.getByText("My qBittorrent"));
+
+ // Save is gated on a passed test or "Save anyway"; toggle Save anyway so
+ // we can save without exercising the (mocked) test mutation.
+ await userEvent.click(screen.getByLabelText(/save anyway/i));
+
+ await userEvent.click(screen.getByRole("button", { name: "Save" }));
+
+ expect(saveServiceMutate).toHaveBeenCalledTimes(1);
+ const saved = saveServiceMutate.mock.calls[0][0];
+ // Must target qBittorrent's id AND carry qBittorrent's own name/config —
+ // not the stale Jellyfin values from the previously selected service.
+ expect(saved.id).toBe("svc-qb");
+ expect(saved.service_type).toBe("qbittorrent");
+ expect(saved.name).toBe("My qBittorrent");
+ expect(saved.config).toEqual({
+ base_url: "https://qb.example.com",
+ timeout_seconds: 60,
+ });
+ });
+});
diff --git a/frontend/src/pages/__tests__/Settings.test.tsx b/frontend/src/pages/__tests__/Settings.test.tsx
index debb550..6652878 100644
--- a/frontend/src/pages/__tests__/Settings.test.tsx
+++ b/frontend/src/pages/__tests__/Settings.test.tsx
@@ -1,17 +1,16 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
+import { MemoryRouter } from "react-router-dom";
import { Settings } from "../Settings";
import type { MonitoringMachine } from "../../types";
const saveMachineMutate = vi.fn().mockResolvedValue({});
const deleteMachineMutate = vi.fn();
-const testSSHMutate = vi
- .fn()
- .mockResolvedValue({
- message: "SSH auth succeeded",
- known_hosts_updated: true,
- });
+const testSSHMutate = vi.fn().mockResolvedValue({
+ message: "SSH auth succeeded",
+ known_hosts_updated: true,
+});
let machines: MonitoringMachine[] = [];
@@ -66,14 +65,21 @@ beforeEach(() => {
describe("Settings", () => {
it("renders the machine list from the mocked store", () => {
machines = [localMachine()];
- render();
- // The rail row caption (mode · enabled) is unique to the selection rail.
+ render(
+
+
+ ,
+ );
expect(screen.getByText("local · Enabled")).toBeInTheDocument();
});
it("saves a machine via the editor dialog (controlled useState parity)", async () => {
machines = [localMachine()];
- render();
+ render(
+
+
+ ,
+ );
// The detail-pane "Edit" has visible text "Edit"; the rail hover edit
// affordance is icon-only (aria-label "Edit") — disambiguate by text.
@@ -99,7 +105,11 @@ describe("Settings", () => {
it("deletes a machine through the confirm dialog", async () => {
machines = [localMachine()];
- render();
+ render(
+
+
+ ,
+ );
// Detail-pane "Delete" opens the confirm dialog.
await userEvent.click(screen.getByRole("button", { name: "Delete" }));