fix: reset service editor on switch + add service-page settings shortcut

Settings.tsx: ServiceConfigEditor derived editable state (name, config,
secrets) from the instance prop via useState, but the parent rendered it
without a key. Switching services in the rail reused the same component, so
name/config stayed pinned to the previously selected service while
instance.id/service_type (read live from props) pointed at the new one —
saving then wrote the stale values onto the wrong row (e.g. saving qBittorrent
renamed it "Jellyfin" with Jellyfin's URL). Add key={selectedService.id} so
the editor remounts and resets on switch.

ServicePage: add a Settings shortcut in the header that deep-links to
/settings?tab=services&service=<id>. Settings now reads tab + service query
params (useSearchParams) to open the Services tab with that service
pre-selected, via a new initialServiceId prop on ServicesAdminCard.

Tests: new Settings.services.test.tsx regression test (fails without the key,
passes with it); wrap existing Settings tests in MemoryRouter since Settings
now uses useSearchParams. 166/166 frontend tests pass; typecheck + ESLint clean.
This commit is contained in:
Developer
2026-07-11 11:54:18 +00:00
parent 84dcf9e010
commit dad2202756
8 changed files with 214 additions and 27 deletions
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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
+17 -1
View File
@@ -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() {
<h2 className="text-xl font-semibold">{instance.name}</h2>
<p className="text-sm text-muted-foreground">{binding.description}</p>
</div>
<Badge variant="outline">{binding.name}</Badge>
<div className="flex items-center gap-2">
<Badge variant="outline">{binding.name}</Badge>
<Button
variant="outline"
size="sm"
onClick={() =>
navigate(
`/settings?tab=services&service=${encodeURIComponent(instance.id)}`,
)
}
>
<SettingsIcon className="mr-1 h-3 w-3" />
Settings
</Button>
</div>
</div>
{/* Instance tabs (only when >1 enabled sibling) */}
+20 -4
View File
@@ -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<SettingsTab>("machines");
const [searchParams] = useSearchParams();
const [tab, setTab] = useState<SettingsTab>(
(searchParams.get("tab") as SettingsTab | null) ?? "machines",
);
const initialServiceId = searchParams.get("service") ?? "";
const [deleteMachineId, setDeleteMachineId] = useState<string | null>(null);
const [selectedSSHKeyId, setSelectedSSHKeyId] = useState("");
const [sshValidationMessage, setSSHValidationMessage] = useState("");
@@ -1195,7 +1200,9 @@ export function Settings() {
onSelectKeyId={setSelectedSSHKeyId}
/>
)}
{tab === "services" && <ServicesAdminCard />}
{tab === "services" && (
<ServicesAdminCard initialServiceId={initialServiceId} />
)}
{tab === "danger" && <ResetLocalDatabaseCard />}
</TabbedCard>
{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 ? (
<ServiceConfigEditor
// Remount on service switch so useState initializers (name, config,
// secrets) re-run for the new instance. Without this key, switching
// services in the rail keeps the previous service's editable state
// and a save writes stale name/config onto the new service's row.
key={selectedService.id}
instance={selectedService}
typeInfo={selectedTypeInfo}
/>
@@ -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
+5 -4
View File
@@ -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
@@ -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(
<MemoryRouter initialEntries={["/settings?tab=services"]}>
<Settings />
</MemoryRouter>,
);
// 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,
});
});
});
+20 -10
View File
@@ -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(<Settings />);
// The rail row caption (mode · enabled) is unique to the selection rail.
render(
<MemoryRouter>
<Settings />
</MemoryRouter>,
);
expect(screen.getByText("local · Enabled")).toBeInTheDocument();
});
it("saves a machine via the editor dialog (controlled useState parity)", async () => {
machines = [localMachine()];
render(<Settings />);
render(
<MemoryRouter>
<Settings />
</MemoryRouter>,
);
// 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(<Settings />);
render(
<MemoryRouter>
<Settings />
</MemoryRouter>,
);
// Detail-pane "Delete" opens the confirm dialog.
await userEvent.click(screen.getByRole("button", { name: "Delete" }));